diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..849517e --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,15 @@ +name: Build +on: + pull_request: { types: [opened, reopened, synchronize, ready_for_review] } + push: { branches: [ main ] } + +jobs: + build: + runs-on: ubuntu-latest + container: swift:6.0-jammy + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Build + run: swift builds + diff --git a/Package.swift b/Package.swift index d8168d7..5953c93 100644 --- a/Package.swift +++ b/Package.swift @@ -1,5 +1,4 @@ -// swift-tools-version:6.0 - +// swift-tools-version: 6.0 import PackageDescription let package = Package( @@ -8,13 +7,13 @@ let package = Package( .macOS(.v13) ], products: [ - //Vapor client for Firebase Cloud Messaging + // Vapor client for Firebase Cloud Messaging .library(name: "FCM", targets: ["FCM"]), ], dependencies: [ // 💧 A server-side Swift web framework. .package(url: "https://github.com/vapor/vapor.git", from: "4.0.0"), - .package(url: "https://github.com/vapor/jwt.git", exact: "5.0.0-rc.1"), + .package(url: "https://github.com/vapor/jwt.git", from: "5.0.0"), ], targets: [ .target(name: "FCM", dependencies: [ diff --git a/README.md b/README.md index 3dfe525..d4758d7 100644 --- a/README.md +++ b/README.md @@ -106,14 +106,13 @@ Here's an example route handler with push notification sending using token ```swift import FCM -func routes(_ app: Application) throws { - app.get("testfcm") { req -> EventLoopFuture in +func routes(_ app: Application) async throws { + app.get("testfcm") { req async throws -> String in let token = "" // get it from iOS/Android SDK let notification = FCMNotification(title: "Vapor is awesome!", body: "Swift one love! ❤️") let message = FCMMessage(token: token, notification: notification) - return req.fcm.send(message, on: req.eventLoop).map { name in - return "Just sent: \(name)" - } + let name = try await req.fcm.send(message, on: req.eventLoop) + return "Just sent: \(name)" } } ``` @@ -171,13 +170,12 @@ Next steps are optional ```swift /// The simplest way /// .env here means that FCM_SERVER_KEY and FCM_APP_BUNDLE_ID will be used -application.fcm.registerAPNS(.env, tokens: "token1", "token3", ..., "token100").flatMap { tokens in - /// `tokens` is array of `APNSToFirebaseToken` structs - /// which contains: - /// registration_token - Firebase token - /// apns_token - APNS token - /// isRegistered - boolean value which indicates if registration was successful -} +let tokens = try await application.fcm.registerAPNS(.env, tokens: "token1", "token3", ..., "token100") +/// `tokens` is array of `APNSToFirebaseToken` structs +/// which contains: +/// registration_token - Firebase token +/// apns_token - APNS token +/// isRegistered - boolean value which indicates if registration was successful /// instead of .env you could declare your own identifier extension RegisterAPNSID { @@ -185,15 +183,13 @@ extension RegisterAPNSID { } /// Advanced way -application.fcm.registerAPNS( +let tokens = try await application.fcm.registerAPNS( appBundleId: String, // iOS app bundle identifier serverKey: String?, // optional server key, if nil then env variable will be used sandbox: Bool, // optional sandbox key, false by default - tokens: [String], // an array of APNS tokens - on: EventLoop? // optional event loop, if nil then application.eventLoopGroup.next() will be used -).flatMap { tokens in - /// the same as in above example -} + tokens: [String] +) +/// the same as in above example ``` > 💡 Please note that push token taken from Xcode while debugging is for `sandbox`, so either use `.envSandbox` or don't forget to set `sandbox: true` diff --git a/Sources/FCM/FCMAndroidConfig/FCMAndroidConfig.swift b/Sources/FCM/FCMAndroidConfig/FCMAndroidConfig.swift index 08984e4..87c200f 100644 --- a/Sources/FCM/FCMAndroidConfig/FCMAndroidConfig.swift +++ b/Sources/FCM/FCMAndroidConfig/FCMAndroidConfig.swift @@ -1,7 +1,16 @@ -public struct FCMAndroidConfig: Codable, Equatable { +public struct FCMAndroidConfig: Sendable, Codable, Equatable { + enum CodingKeys: String, CodingKey { + case collapseKey = "collapse_key" + case priority + case ttl + case restrictedPackageName = "restricted_package_name" + case data + case notification + } + /// An identifier of a group of messages that can be collapsed, so that only the last message gets sent when delivery can be resumed. /// A maximum of 4 different collapse keys is allowed at any given time. - public var collapse_key: String? + public var collapseKey: String? /// Message priority. Can take "normal" and "high" values. /// For more information, see Setting the priority of a message. @@ -20,7 +29,7 @@ public struct FCMAndroidConfig: Codable, Equatable { public var ttl: String? /// Package name of the application where the registration tokens must match in order to receive the message. - public var restricted_package_name: String? + public var restrictedPackageName: String? /// Arbitrary key/value payload. /// If present, it will override FCMMessage.data. @@ -30,16 +39,18 @@ public struct FCMAndroidConfig: Codable, Equatable { public var notification: FCMAndroidNotification? /// Public Initializer - public init(collapse_key: String? = nil, - priority: FCMAndroidMessagePriority = .normal, - ttl: String? = nil, - restricted_package_name: String? = nil, - data: [String: String]? = nil, - notification: FCMAndroidNotification? = nil) { - self.collapse_key = collapse_key + init( + collapseKey: String? = nil, + priority: FCMAndroidMessagePriority, + ttl: String? = nil, + restrictedPackageName: String? = nil, + data: [String : String]? = nil, + notification: FCMAndroidNotification? = nil + ) { + self.collapseKey = collapseKey self.priority = priority self.ttl = ttl - self.restricted_package_name = restricted_package_name + self.restrictedPackageName = restrictedPackageName self.data = data self.notification = notification } diff --git a/Sources/FCM/FCMAndroidConfig/FCMAndroidMessagePriority.swift b/Sources/FCM/FCMAndroidConfig/FCMAndroidMessagePriority.swift index 195ec44..d7931da 100644 --- a/Sources/FCM/FCMAndroidConfig/FCMAndroidMessagePriority.swift +++ b/Sources/FCM/FCMAndroidConfig/FCMAndroidMessagePriority.swift @@ -1,4 +1,4 @@ -public enum FCMAndroidMessagePriority: String, Codable, Equatable { +public enum FCMAndroidMessagePriority: String, Sendable, Codable, Equatable { /// Default priority for data messages. /// Normal priority messages won't open network connections on a sleeping device, /// and their delivery may be delayed to conserve the battery. diff --git a/Sources/FCM/FCMAndroidConfig/FCMAndroidNotification.swift b/Sources/FCM/FCMAndroidConfig/FCMAndroidNotification.swift index 1ca9590..0bcdac3 100644 --- a/Sources/FCM/FCMAndroidConfig/FCMAndroidNotification.swift +++ b/Sources/FCM/FCMAndroidConfig/FCMAndroidNotification.swift @@ -1,4 +1,32 @@ -public struct FCMAndroidNotification: Codable, Equatable { +public struct FCMAndroidNotification: Sendable, Codable, Equatable { + enum CodingKeys: String, CodingKey { + case title + case body + case icon + case color + case sound + case tag + case clickAction = "click_action" + case bodyLocalizationKey = "body_loc_key" + case bodyLocalizationArgs = "body_loc_args" + case titleLocalizationKey = "title_loc_key" + case titleLocalizationArgs = "title_loc_args" + case channelID = "channel_id" + case ticker + case sticky + case eventTime = "event_time" + case localOnly = "local_only" + case notificationPriority = "notification_priority" + case defaultSound = "default_sound" + case defaultVibrateTimings = "default_vibrate_timings" + case defaultLightSettings = "default_light_settings" + case vibrateTimings = "vibrate_timings" + case visibility + case notificationCount = "notification_count" + case lightSettings = "light_settings" + case image + } + /// The notification's title. /// If present, it will override FCMNotification.title. public var title: String? @@ -24,33 +52,33 @@ public struct FCMAndroidNotification: Codable, Equatable { /// The action associated with a user click on the notification. /// If specified, an activity with a matching intent filter is launched when a user clicks on the notification. - public var click_action: String? + public var clickAction: String? /// The key to the body string in the app's string resources /// to use to localize the body text to the user's current localization. /// See String Resources for more information. - public var body_loc_key: String? + public var bodyLocalizationKey: String? /// Variable string values to be used in place of the format specifiers /// in body_loc_key to use to localize the body text to the user's current localization. /// See Formatting and Styling for more information. - public var body_loc_args: [String]? + public var bodyLocalizationArgs: [String]? /// The key to the title string in the app's string resources /// to use to localize the title text to the user's current localization. /// See String Resources for more information. - public var title_loc_key: String? + public var titleLocalizationKey: String? /// Variable string values to be used in place of the format specifiers /// in title_loc_key to use to localize the title text to the user's current localization. /// See Formatting and Styling for more information. - public var title_loc_args: [String]? + public var titleLocalizationArgs: [String]? /// The notification's channel id (new in Android O). /// The app must create a channel with this channel ID before any notification with this channel ID is received. /// If you don't send this channel ID in the request, or if the channel ID provided has not yet been created by the app, /// FCM uses the channel ID specified in the app manifest. - public var channel_id: String? + public var channelID: String? /// Sets the "ticker" text, which is sent to accessibility services. /// Prior to API level 21 (Lollipop), sets the text that is displayed in the status bar when the notification first arrives. @@ -64,12 +92,12 @@ public struct FCMAndroidNotification: Codable, Equatable { /// A point in time is represented using protobuf.Timestamp. /// A timestamp in RFC3339 UTC "Zulu" format, accurate to nanoseconds. /// Example: "2014-10-02T15:01:23.045123456Z". - public var event_time: String? + public var eventTime: String? /// Set whether or not this notification is relevant only to the current device. /// Some notifications can be bridged to other devices for remote display, such as a Wear OS watch. /// This hint can be set to recommend this notification not be bridged. See Wear OS guides - public var local_only: Bool? + public var localOnly: Bool? /// Set the relative priority for this notification. /// Priority is an indication of how much of the user's attention should be consumed by this notification. @@ -78,23 +106,23 @@ public struct FCMAndroidNotification: Codable, Equatable { /// Note this priority differs from AndroidMessagePriority. /// This priority is processed by the client after the message has been delivered, /// whereas AndroidMessagePriority is an FCM concept that controls when the message is delivered. - public var notification_priority: FCMAndroidNotificationPriority? + public var notificationPriority: FCMAndroidNotificationPriority? /// If set to true, use the Android framework's default sound for the notification. /// Default values are specified in config.xml. - public var default_sound: Bool? + public var defaultSound: Bool? /// If set to true, use the Android framework's default vibrate pattern for the notification. /// Default values are specified in config.xml. /// If default_vibrate_timings is set to true and vibrate_timings is also set, /// the default value is used instead of the user-specified vibrate_timings. - public var default_vibrate_timings: Bool? + public var defaultVibrateTimings: Bool? /// If set to true, use the Android framework's default LED light settings for the notification. /// Default values are specified in config.xml. /// If default_light_settings is set to true and light_settings is also set, /// the user-specified light_settings is used instead of the default value. - public var default_light_settings: Bool? + public var defaultLightSettings: Bool? /// Set the vibration pattern to use. /// Pass in an array of protobuf.Duration to turn on or off the vibrator. @@ -104,7 +132,7 @@ public struct FCMAndroidNotification: Codable, Equatable { /// If vibrate_timings is set and default_vibrate_timings is set to true, /// the default value is used instead of the user-specified vibrate_timings. /// A duration in seconds with up to nine fractional digits, terminated by 's'. Example: "3.5s". - public var vibrate_timings: [String]? + public var vibrateTimings: [String]? /// Set the Notification.visibility of the notification. public var visibility: FCMAndroidNotificationVisibility? @@ -115,66 +143,67 @@ public struct FCMAndroidNotification: Codable, Equatable { /// but you want the count here to represent the number of total new messages. /// If zero or unspecified, systems that support badging use the default, /// which is to increment a number displayed on the long-press menu each time a new notification arrives. - public var notification_count: Int? + public var notificationCount: Int? /// Settings to control the notification's LED blinking rate and color if LED is available on the device. /// The total blinking time is controlled by the OS. - public var light_settings: FCMAndroidNotificationLightSettings? + public var lightSettings: FCMAndroidNotificationLightSettings? /// Contains the URL of an image that is going to be displayed in a notification. /// If present, it will override google.firebase.fcm.v1.Notification.image. public var image: String? - - /// Public Initializer - public init(title: String? = nil, - body: String? = nil, - icon: String? = nil, - color: String? = nil, - sound: String? = nil, - tag: String? = nil, - click_action: String? = nil, - body_loc_key: String? = nil, - body_loc_args: [String]? = nil, - title_loc_key: String? = nil, - title_loc_args: [String]? = nil, - channel_id: String? = nil, - ticker: String? = nil, - sticky: Bool? = nil, - event_time: String? = nil, - local_only: Bool? = nil, - notification_priority: FCMAndroidNotificationPriority? = nil, - default_sound: Bool? = nil, - default_vibrate_timings: Bool? = nil, - default_light_settings: Bool? = nil, - vibrate_timings: [String]? = nil, - visibility: FCMAndroidNotificationVisibility? = nil, - notification_count: Int? = nil, - light_settings: FCMAndroidNotificationLightSettings? = nil, - image: String? = nil) { + + init( + title: String? = nil, + body: String? = nil, + icon: String? = nil, + color: String? = nil, + sound: String? = nil, + tag: String? = nil, + clickAction: String? = nil, + bodyLocalizationKey: String? = nil, + bodyLocalizationArgs: [String]? = nil, + titleLocalizationKey: String? = nil, + titleLocalizationArgs: [String]? = nil, + channelID: String? = nil, + ticker: String? = nil, + sticky: Bool? = nil, + eventTime: String? = nil, + localOnly: Bool? = nil, + notificationPriority: FCMAndroidNotificationPriority? = nil, + defaultSound: Bool? = nil, + defaultVibrateTimings: Bool? = nil, + defaultLightSettings: Bool? = nil, + vibrateTimings: [String]? = nil, + visibility: FCMAndroidNotificationVisibility? = nil, + notificationCount: Int? = nil, + lightSettings: FCMAndroidNotificationLightSettings? = nil, + image: String? = nil + ) { self.title = title self.body = body self.icon = icon self.color = color self.sound = sound self.tag = tag - self.click_action = click_action - self.body_loc_key = body_loc_key - self.body_loc_args = body_loc_args - self.title_loc_key = title_loc_key - self.title_loc_args = title_loc_args - self.channel_id = channel_id + self.clickAction = clickAction + self.bodyLocalizationKey = bodyLocalizationKey + self.bodyLocalizationArgs = bodyLocalizationArgs + self.titleLocalizationKey = titleLocalizationKey + self.titleLocalizationArgs = titleLocalizationArgs + self.channelID = channelID self.ticker = ticker self.sticky = sticky - self.event_time = event_time - self.local_only = local_only - self.notification_priority = notification_priority - self.default_sound = default_sound - self.default_vibrate_timings = default_vibrate_timings - self.default_light_settings = default_light_settings - self.vibrate_timings = vibrate_timings + self.eventTime = eventTime + self.localOnly = localOnly + self.notificationPriority = notificationPriority + self.defaultSound = defaultSound + self.defaultVibrateTimings = defaultVibrateTimings + self.defaultLightSettings = defaultLightSettings + self.vibrateTimings = vibrateTimings self.visibility = visibility - self.notification_count = notification_count - self.light_settings = light_settings + self.notificationCount = notificationCount + self.lightSettings = lightSettings self.image = image } } diff --git a/Sources/FCM/FCMAndroidConfig/FCMAndroidNotificationLightSettings.swift b/Sources/FCM/FCMAndroidConfig/FCMAndroidNotificationLightSettings.swift index 255a73a..473e91e 100644 --- a/Sources/FCM/FCMAndroidConfig/FCMAndroidNotificationLightSettings.swift +++ b/Sources/FCM/FCMAndroidConfig/FCMAndroidNotificationLightSettings.swift @@ -7,34 +7,38 @@ public typealias FCMAndroidNotificationLightSettingsColor = FCMAndroidNotificationLightSettings.Color -public struct FCMAndroidNotificationLightSettings: Codable, Equatable { +public struct FCMAndroidNotificationLightSettings: Sendable, Codable, Equatable { + enum CodingKeys: String, CodingKey { + case color + case lightOnDuration = "light_on_duration" + case lightOffDuration = "light_off_duration" + } + /// Required. Set color of the LED with google.type.Color. public var color: Color /// Required. Along with light_off_duration, define the blink rate of LED flashes. /// Resolution defined by proto.Duration /// A duration in seconds with up to nine fractional digits, terminated by 's'. Example: "3.5s". - public var light_on_duration: String + public var lightOnDuration: String /// Required. Along with light_on_duration, define the blink rate of LED flashes. /// Resolution defined by proto.Duration /// A duration in seconds with up to nine fractional digits, terminated by 's'. Example: "3.5s". - public var light_off_duration: String + public var lightOffDuration: String - public init (color: Color, - light_on_duration: String, - light_off_duration: String) { + init(color: Color, lightOnDuration: String, lightOffDuration: String) { self.color = color - self.light_on_duration = light_on_duration - self.light_off_duration = light_off_duration + self.lightOnDuration = lightOnDuration + self.lightOffDuration = lightOffDuration } } extension FCMAndroidNotificationLightSettings { - public struct Color: Codable, Equatable { + public struct Color: Codable, Sendable, Equatable { public var red, green, blue, alpha: Float - public init (red: Float, green: Float, blue: Float, alpha: Float) { + public init(red: Float, green: Float, blue: Float, alpha: Float) { self.red = red self.green = green self.blue = blue diff --git a/Sources/FCM/FCMAndroidConfig/FCMAndroidNotificationPriority.swift b/Sources/FCM/FCMAndroidConfig/FCMAndroidNotificationPriority.swift index e32d13d..736070b 100644 --- a/Sources/FCM/FCMAndroidConfig/FCMAndroidNotificationPriority.swift +++ b/Sources/FCM/FCMAndroidConfig/FCMAndroidNotificationPriority.swift @@ -5,7 +5,7 @@ // Created by Oleh Hudeichuk on 13.12.2019. // -public enum FCMAndroidNotificationPriority: String, Codable, Equatable { +public enum FCMAndroidNotificationPriority: String, Sendable, Codable, Equatable { /// If priority is unspecified, notification priority is set to PRIORITY_DEFAULT. case unspecified = "PRIORITY_UNSPECIFIED" diff --git a/Sources/FCM/FCMAndroidConfig/FCMAndroidNotificationVisibility.swift b/Sources/FCM/FCMAndroidConfig/FCMAndroidNotificationVisibility.swift index e7c0d56..e076cbb 100644 --- a/Sources/FCM/FCMAndroidConfig/FCMAndroidNotificationVisibility.swift +++ b/Sources/FCM/FCMAndroidConfig/FCMAndroidNotificationVisibility.swift @@ -5,7 +5,7 @@ // Created by Oleh Hudeichuk on 13.12.2019. // -public enum FCMAndroidNotificationVisibility: String, Codable, Equatable { +public enum FCMAndroidNotificationVisibility: String, Sendable, Codable, Equatable { /// If unspecified, default to Visibility.PRIVATE. case unspecified = "VISIBILITY_UNSPECIFIED" diff --git a/Sources/FCM/FCMApnsConfig/FCMApnsAlert.swift b/Sources/FCM/FCMApnsConfig/FCMApnsAlert.swift index bcd273c..ff355e3 100644 --- a/Sources/FCM/FCMApnsConfig/FCMApnsAlert.swift +++ b/Sources/FCM/FCMApnsConfig/FCMApnsAlert.swift @@ -1,4 +1,4 @@ -public struct FCMApnsAlert: Codable, Equatable { +public struct FCMApnsAlert: Sendable, Codable, Equatable { /// The title of the notification. /// Apple Watch displays this string in the short look notification interface. /// Specify a string that is quickly understood by the user. diff --git a/Sources/FCM/FCMApnsConfig/FCMApnsAlertOrString.swift b/Sources/FCM/FCMApnsConfig/FCMApnsAlertOrString.swift index ab75da8..8ace302 100644 --- a/Sources/FCM/FCMApnsConfig/FCMApnsAlertOrString.swift +++ b/Sources/FCM/FCMApnsConfig/FCMApnsAlertOrString.swift @@ -1,5 +1,5 @@ /// Internal helper for different alert payload types -public enum FCMApnsAlertOrString: Codable, Equatable { +public enum FCMApnsAlertOrString: Sendable, Codable, Equatable { public init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() if let string = try? container.decode(String.self) { diff --git a/Sources/FCM/FCMApnsConfig/FCMApnsApsObject.swift b/Sources/FCM/FCMApnsConfig/FCMApnsApsObject.swift index 9f463c3..abd64e4 100644 --- a/Sources/FCM/FCMApnsConfig/FCMApnsApsObject.swift +++ b/Sources/FCM/FCMApnsConfig/FCMApnsApsObject.swift @@ -1,6 +1,6 @@ // The following struct is based on // https://developer.apple.com/documentation/usernotifications/setting_up_a_remote_notification_server/generating_a_remote_notification -public struct FCMApnsApsObject: Codable, Equatable { +public struct FCMApnsApsObject: Sendable, Codable, Equatable { /// The information for displaying an alert. /// A dictionary is recommended. /// If you specify a string, the alert displays your string as the body text. diff --git a/Sources/FCM/FCMApnsConfig/FCMApnsConfig.swift b/Sources/FCM/FCMApnsConfig/FCMApnsConfig.swift index c7a3728..cdd2ddb 100644 --- a/Sources/FCM/FCMApnsConfig/FCMApnsConfig.swift +++ b/Sources/FCM/FCMApnsConfig/FCMApnsConfig.swift @@ -1,6 +1,6 @@ import Foundation -final public class FCMApnsConfig

: Codable where P: FCMApnsPayloadProtocol { +public struct FCMApnsConfig

: Sendable, Codable where P: FCMApnsPayloadProtocol & Sendable { /// HTTP request headers defined in Apple Push Notification Service. /// Refer to APNs request headers for supported headers, e.g. "apns-priority": "10". public var headers: [String: String] @@ -12,7 +12,7 @@ final public class FCMApnsConfig

: Codable where P: FCMApnsPayloadProtocol { /// FCM Options to send meta data public var options: FCMOptions? - //MARK: - Public Initializers + // MARK: - Public Initializers /// Use this if you need custom payload /// Your payload should conform to FCMApnsPayloadProtocol @@ -31,8 +31,8 @@ final public class FCMApnsConfig

: Codable where P: FCMApnsPayloadProtocol { extension FCMApnsConfig where P == FCMApnsPayload { /// Use this if you need only aps object - public convenience init(headers: [String: String]? = nil, aps: FCMApnsApsObject? = nil, options: FCMOptions? = nil) { - if let aps = aps { + public init(headers: [String: String]? = nil, aps: FCMApnsApsObject? = nil, options: FCMOptions? = nil) { + if let aps { self.init(headers: headers, payload: FCMApnsPayload(aps: aps), options: options) } else { self.init(headers: headers, payload: FCMApnsPayload(), options: options) @@ -41,6 +41,6 @@ extension FCMApnsConfig where P == FCMApnsPayload { /// Returns an instance with default values public static var `default`: FCMApnsConfig { - return FCMApnsConfig() + FCMApnsConfig() } } diff --git a/Sources/FCM/FCMApnsConfig/FCMApnsPayloadProtocol.swift b/Sources/FCM/FCMApnsConfig/FCMApnsPayloadProtocol.swift index ad8b48b..c844cc2 100644 --- a/Sources/FCM/FCMApnsConfig/FCMApnsPayloadProtocol.swift +++ b/Sources/FCM/FCMApnsConfig/FCMApnsPayloadProtocol.swift @@ -1,15 +1,17 @@ /// Use it for your custom payload model /// /// Example code: -/// struct MyCustomPayload: FCMApnsPayloadProtocol { -/// var aps: FCMApnsApsObject -/// var myCustomKey: String +/// ```swift +/// struct MyCustomPayload: FCMApnsPayloadProtocol { +/// var aps: FCMApnsApsObject +/// var myCustomKey: String /// -/// init(aps: FCMApnsApsObject, myCustomKey: String) { -/// self.aps = aps -/// self.myCustomKey = myCustomKey -/// } +/// init(aps: FCMApnsApsObject, myCustomKey: String) { +/// self.aps = aps +/// self.myCustomKey = myCustomKey /// } -public protocol FCMApnsPayloadProtocol: Codable, Equatable { +/// } +/// ``` +public protocol FCMApnsPayloadProtocol: Sendable, Codable, Equatable { var aps: FCMApnsApsObject { get set } } diff --git a/Sources/FCM/FCMApnsConfig/FCMOptions.swift b/Sources/FCM/FCMApnsConfig/FCMOptions.swift index 8ab4e00..62eb42f 100644 --- a/Sources/FCM/FCMApnsConfig/FCMOptions.swift +++ b/Sources/FCM/FCMApnsConfig/FCMOptions.swift @@ -1,4 +1,4 @@ -public struct FCMOptions: Codable, Equatable, FCMOptionsProtocol { +public struct FCMOptions: Sendable, Codable, Equatable, FCMOptionsProtocol { /// Label associated with the message's analytics data. public var analyticsLabel: String /// Image to be shown in push messages diff --git a/Sources/FCM/FCMConfiguration.swift b/Sources/FCM/FCMConfiguration.swift index a55d60c..3e9ea4c 100644 --- a/Sources/FCM/FCMConfiguration.swift +++ b/Sources/FCM/FCMConfiguration.swift @@ -1,7 +1,7 @@ import Foundation import Vapor -public struct FCMConfiguration : @unchecked Sendable { +public struct FCMConfiguration: Sendable { let email, projectId, key: String let serverKey, senderId: String? @@ -10,7 +10,7 @@ public struct FCMConfiguration : @unchecked Sendable { public var apnsDefaultConfig: FCMApnsConfig? public var androidDefaultConfig: FCMAndroidConfig? public var webpushDefaultConfig: FCMWebpushConfig? - + // MARK: Initializers public init (email: String, projectId: String, key: String, serverKey: String? = nil, senderId: String? = nil) { @@ -59,7 +59,7 @@ public struct FCMConfiguration : @unchecked Sendable { let email = Environment.get("FCM_EMAIL"), let projectId = Environment.get("FCM_PROJECT_ID"), let keyPath = Environment.get("FCM_KEY_PATH") - else { + else { fatalError("FCM envCredentials not set") } let serverKey = Environment.get("FCM_SERVER_KEY") @@ -86,7 +86,7 @@ public struct FCMConfiguration : @unchecked Sendable { let email = Environment.get("FCM_EMAIL"), let projectId = Environment.get("FCM_PROJECT_ID"), let rawPrivateKey = Environment.get("FCM_PRIVATE_KEY") - else { + else { fatalError("FCM envCredentials not set") } return .init(email: email, projectId: projectId, key: rawPrivateKey.replacingOccurrences(of: "\\n", with: "\n")) diff --git a/Sources/FCM/FCMError.swift b/Sources/FCM/FCMError.swift index 2ae1551..ede9ceb 100644 --- a/Sources/FCM/FCMError.swift +++ b/Sources/FCM/FCMError.swift @@ -42,15 +42,6 @@ public struct FCMError: Error, Decodable { } } -extension EventLoopFuture where Value == ClientResponse { - func validate() -> EventLoopFuture { - return flatMapThrowing { (response) in - try response.validate() - return response - } - } -} - extension ClientResponse { func validate() throws { guard 200 ..< 300 ~= self.status.code else { diff --git a/Sources/FCM/FCMMessage.swift b/Sources/FCM/FCMMessage.swift index 697bc50..b7b1c1f 100644 --- a/Sources/FCM/FCMMessage.swift +++ b/Sources/FCM/FCMMessage.swift @@ -2,14 +2,14 @@ import Foundation public typealias FCMMessageDefault = FCMMessage -public class FCMMessage: @unchecked Sendable, Codable where APNSPayload: FCMApnsPayloadProtocol { +public struct FCMMessage: Sendable, Codable where APNSPayload: FCMApnsPayloadProtocol { /// Output Only. /// The identifier of the message sent, /// in the format of projects/*/messages/{message_id}. public var name: String /// Input only. Arbitrary key/value payload. - public var data: [String: String] = [:] + public var data: [String: String] /// Input only. /// Basic notification template to use across all platforms. @@ -27,7 +27,7 @@ public class FCMMessage: @unchecked Sendable, Codable where APNSPay /// Apple Push Notification Service specific options. public var apns: FCMApnsConfig? - //MARK: - Union field target. Required. Input only. Target to send a message to. target can be only one of the following: + // MARK: - Union field target. Required. Input only. Target to send a message to. target can be only one of the following: /// Registration token to send a message to. public var token: String? @@ -48,13 +48,13 @@ public class FCMMessage: @unchecked Sendable, Codable where APNSPay apns: FCMApnsConfig? = nil) { self.token = token self.notification = notification - if let data = data { - self.data = data - } + self.data = data ?? [:] self.name = name ?? UUID().uuidString self.android = android self.webpush = webpush self.apns = apns + self.topic = nil + self.condition = nil } /// Initialization with topic @@ -67,13 +67,13 @@ public class FCMMessage: @unchecked Sendable, Codable where APNSPay apns: FCMApnsConfig? = nil) { self.topic = topic self.notification = notification - if let data = data { - self.data = data - } + self.data = data ?? [:] self.name = name ?? UUID().uuidString self.android = android self.webpush = webpush self.apns = apns + self.condition = nil + self.token = nil } /// Initialization with condition @@ -87,19 +87,19 @@ public class FCMMessage: @unchecked Sendable, Codable where APNSPay { self.condition = condition self.notification = notification - if let data = data { - self.data = data - } + self.data = data ?? [:] self.name = name ?? UUID().uuidString self.android = android self.webpush = webpush self.apns = apns + self.token = nil + self.topic = nil } } extension FCMMessage where APNSPayload == FCMApnsPayload { /// Initialization with device token - public convenience init(token: String, + public init(token: String, notification: FCMNotification?, data: [String: String]? = nil, name: String? = nil, @@ -115,7 +115,7 @@ extension FCMMessage where APNSPayload == FCMApnsPayload { } /// Initialization with topic - public convenience init(topic: String, + public init(topic: String, notification: FCMNotification?, data: [String: String]? = nil, name: String? = nil, @@ -131,7 +131,7 @@ extension FCMMessage where APNSPayload == FCMApnsPayload { } /// Initialization with condition - public convenience init(condition: String, + public init(condition: String, notification: FCMNotification?, data: [String: String]? = nil, name: String? = nil, diff --git a/Sources/FCM/FCMNotification.swift b/Sources/FCM/FCMNotification.swift index b492343..9b8f996 100644 --- a/Sources/FCM/FCMNotification.swift +++ b/Sources/FCM/FCMNotification.swift @@ -1,4 +1,4 @@ -public struct FCMNotification: Codable { +public struct FCMNotification: Sendable, Codable { /// The notification's title. var title: String diff --git a/Sources/FCM/FCMWebpushConfig.swift b/Sources/FCM/FCMWebpushConfig.swift index 67262dd..a90021e 100644 --- a/Sources/FCM/FCMWebpushConfig.swift +++ b/Sources/FCM/FCMWebpushConfig.swift @@ -1,4 +1,4 @@ -public struct FCMWebpushConfig: Codable { +public struct FCMWebpushConfig: Sendable, Codable { /// HTTP headers defined in webpush protocol. /// Refer to Webpush protocol for supported headers, e.g. "TTL": "15". public var headers: [String: String]? diff --git a/Sources/FCM/Helpers/FCM+DeleteTopic.swift b/Sources/FCM/Helpers/FCM+DeleteTopic.swift index 6f625ec..c66d08d 100644 --- a/Sources/FCM/Helpers/FCM+DeleteTopic.swift +++ b/Sources/FCM/Helpers/FCM+DeleteTopic.swift @@ -6,18 +6,10 @@ extension FCM { try await deleteTopic(name, tokens: tokens) } -// public func deleteTopic(_ name: String, tokens: String..., on eventLoop: EventLoop) -> EventLoopFuture { -// deleteTopic(name, tokens: tokens).hop(to: eventLoop) -// } - public func deleteTopic(_ name: String, tokens: [String]) async throws { try await _deleteTopic(name, tokens: tokens) } -// public func deleteTopic(_ name: String, tokens: [String], on eventLoop: EventLoop) -> EventLoopFuture { -// _deleteTopic(name, tokens: tokens).hop(to: eventLoop) -// } - private func _deleteTopic(_ name: String, tokens: [String]) async throws { guard let configuration = self.configuration else { fatalError("FCM not configured. Use app.fcm.configuration = ...") diff --git a/Sources/FCM/Helpers/FCM+RegisterAPNS.swift b/Sources/FCM/Helpers/FCM+RegisterAPNS.swift index da3ab4a..ad71a52 100644 --- a/Sources/FCM/Helpers/FCM+RegisterAPNS.swift +++ b/Sources/FCM/Helpers/FCM+RegisterAPNS.swift @@ -24,8 +24,7 @@ extension RegisterAPNSID { extension RegisterAPNSID { public static var envSandbox: RegisterAPNSID { - let id: RegisterAPNSID = .env - return .init(appBundleId: id.appBundleId, sandbox: true) + .init(appBundleId: RegisterAPNSID.env.appBundleId, sandbox: true) } } @@ -49,9 +48,9 @@ extension FCM { /// public func registerAPNS( _ id: RegisterAPNSID, - tokens: String..., - on eventLoop: EventLoop? = nil) -> EventLoopFuture<[APNSToFirebaseToken]> { - registerAPNS(appBundleId: id.appBundleId, serverKey: id.serverKey, sandbox: id.sandbox, tokens: tokens, on: eventLoop) + tokens: String... + ) async throws -> [APNSToFirebaseToken] { + try await registerAPNS(appBundleId: id.appBundleId, serverKey: id.serverKey, sandbox: id.sandbox, tokens: tokens) } /// Helper method which registers your pure APNS token in Firebase Cloud Messaging @@ -68,9 +67,9 @@ extension FCM { /// public func registerAPNS( _ id: RegisterAPNSID, - tokens: [String], - on eventLoop: EventLoop? = nil) -> EventLoopFuture<[APNSToFirebaseToken]> { - registerAPNS(appBundleId: id.appBundleId, serverKey: id.serverKey, sandbox: id.sandbox, tokens: tokens, on: eventLoop) + tokens: [String] + ) async throws -> [APNSToFirebaseToken] { + try await registerAPNS(appBundleId: id.appBundleId, serverKey: id.serverKey, sandbox: id.sandbox, tokens: tokens) } /// Helper method which registers your pure APNS token in Firebase Cloud Messaging @@ -79,9 +78,9 @@ extension FCM { appBundleId: String, serverKey: String? = nil, sandbox: Bool = false, - tokens: String..., - on eventLoop: EventLoop? = nil) -> EventLoopFuture<[APNSToFirebaseToken]> { - registerAPNS(appBundleId: appBundleId, serverKey: serverKey, sandbox: sandbox, tokens: tokens, on: eventLoop) + tokens: String... + ) async throws -> [APNSToFirebaseToken] { + try await registerAPNS(appBundleId: appBundleId, serverKey: serverKey, sandbox: sandbox, tokens: tokens) } /// Helper method which registers your pure APNS token in Firebase Cloud Messaging @@ -90,31 +89,34 @@ extension FCM { appBundleId: String, serverKey: String? = nil, sandbox: Bool = false, - tokens: [String], - on eventLoop: EventLoop? = nil) -> EventLoopFuture<[APNSToFirebaseToken]> { - let eventLoop = eventLoop ?? client.eventLoop + tokens: [String] + ) async throws -> [APNSToFirebaseToken] { guard tokens.count <= 100 else { - return eventLoop.makeFailedFuture(Abort(.internalServerError, reason: "FCM: Register APNS: tokens count should be less or equeal 100")) + throw Abort(.internalServerError, reason: "FCM: Register APNS: tokens count should be less or equal 100") } + guard tokens.count > 0 else { - return eventLoop.future([]) + return [] } + guard let configuration = self.configuration else { - #if DEBUG + #if DEBUG fatalError("FCM not configured. Use app.fcm.configuration = ...") - #else - return eventLoop.future([]) - #endif + #else + return [] + #endif } + guard let serverKey = serverKey ?? configuration.serverKey else { - fatalError("FCM: Register APNS: Server Key is missing.") + throw Abort(.internalServerError, reason: "FCM: Register APNS: Server Key is missing.") } + let url = iidURL + "batchImport" - + var headers = HTTPHeaders() headers.add(name: .authorization, value: "key=\(serverKey)") - - return self.client.post(URI(string: url), headers: headers) { (req) in + + let response = try await self.client.post(URI(string: url), headers: headers) { req in struct Payload: Content { let application: String let sandbox: Bool @@ -123,18 +125,19 @@ extension FCM { let payload = Payload(application: appBundleId, sandbox: sandbox, apns_tokens: tokens) try req.content.encode(payload) } - .validate() - .flatMapThrowing { res in + + try response.validate() + + struct Result: Codable { struct Result: Codable { - struct Result: Codable { - let registration_token, apns_token, status: String - } - let results: [Result] - } - let result = try res.content.decode(Result.self) - return result.results.map { - .init(registration_token: $0.registration_token, apns_token: $0.apns_token, isRegistered: $0.status == "OK") + let registration_token, apns_token, status: String } + let results: [Result] + } + + let result = try response.content.decode(Result.self) + return result.results.map { + .init(registration_token: $0.registration_token, apns_token: $0.apns_token, isRegistered: $0.status == "OK") } } } diff --git a/Sources/FCM/Helpers/FCM+SendMessage.swift b/Sources/FCM/Helpers/FCM+SendMessage.swift index 2f95050..08b21ca 100644 --- a/Sources/FCM/Helpers/FCM+SendMessage.swift +++ b/Sources/FCM/Helpers/FCM+SendMessage.swift @@ -10,6 +10,8 @@ extension FCM { guard let configuration = self.configuration else { fatalError("FCM not configured. Use app.fcm.configuration = ...") } + var message = message + if message.apns == nil, let apnsDefaultConfig = apnsDefaultConfig { message.apns = apnsDefaultConfig diff --git a/Tests/FCMTests/FCMTests.swift b/Tests/FCMTests/FCMTests.swift index e4fd554..98e9a5e 100644 --- a/Tests/FCMTests/FCMTests.swift +++ b/Tests/FCMTests/FCMTests.swift @@ -1,8 +1,10 @@ -import XCTest +import Testing @testable import FCM -final class FCMTests: XCTestCase { - func testExample() { - XCTAssertFalse(false) +@Suite("FCM Tests") +struct FCMTests { + @Test + func example() async throws { + #expect(1 == 1) } } diff --git a/Tests/FCMTests/XCTestManifests.swift b/Tests/FCMTests/XCTestManifests.swift deleted file mode 100644 index f627f8a..0000000 --- a/Tests/FCMTests/XCTestManifests.swift +++ /dev/null @@ -1,9 +0,0 @@ -import XCTest - -#if !os(macOS) -public func allTests() -> [XCTestCaseEntry] { - return [ - testCase(FCMTests.allTests), - ] -} -#endif \ No newline at end of file diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift deleted file mode 100644 index 12f892f..0000000 --- a/Tests/LinuxMain.swift +++ /dev/null @@ -1,7 +0,0 @@ -import XCTest - -import FCMTests - -var tests = [XCTestCaseEntry]() -tests += FCMTests.allTests() -XCTMain(tests) \ No newline at end of file