Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 29 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@

An abstract push notification client for Feather CMS.

[![Release: 1.0.0-beta.1](https://img.shields.io/badge/Release-1%2E0%2E0--beta%2E1-F05138)](https://github.com/feather-framework/feather-push/releases/tag/1.0.0-beta.1)
[![Release: 1.0.0-beta.2](https://img.shields.io/badge/Release-1%2E0%2E0--beta%2E2-F05138)](https://github.com/feather-framework/feather-push/releases/tag/1.0.0-beta.2)

## Features

- Topic-based push notification delivery
- Topic- and device-token-based push notification delivery
- Silent and normal delivery modes
- Deep-link and rich notification metadata
- Badge, sound, and notification collapsing support
Expand All @@ -31,7 +31,7 @@ An abstract push notification client for Feather CMS.
Use Swift Package Manager; add the dependency to your `Package.swift` file:

```swift
.package(url: "https://github.com/feather-framework/feather-push", exact: "1.0.0-beta.1"),
.package(url: "https://github.com/feather-framework/feather-push", exact: "1.0.0-beta.2"),
```

Then add `FeatherPush` to your target dependencies:
Expand All @@ -46,6 +46,14 @@ Then add `FeatherPush` to your target dependencies:

API documentation is available at the following link.

`PushClient` accepts a single delivery target for every notification.

Providers do not all support the same targeting model. For example, FCM
supports subscribable topics, while APNs delivers to device tokens and uses
its topic value to identify the application rather than a group of subscribers.
An implementation that cannot handle a target should throw
`PushClientError.unsupportedTarget`.

```swift
let notification = PushNotification(
title: "New message",
Expand All @@ -56,10 +64,24 @@ let notification = PushNotification(
sound: .default
)

try await client.send(
notification: notification,
to: "topic"
)
// Device-token delivery is supported by every PushClient implementation.
func sendDeviceNotification(using client: some PushClient) async throws {
try await client.send(
notification: notification,
to: .deviceToken("device-registration-token")
)
}
```

The same interface can target a provider-managed topic:

```swift
func sendTopicNotification(using client: some PushClient) async throws {
try await client.send(
notification: notification,
to: .topic("messages")
)
}
```

> [!WARNING]
Expand Down
24 changes: 24 additions & 0 deletions Sources/FeatherPush/Client/PushClient.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
//
// PushClient.swift
// feather-push
//
// Created by Binary Birds on 2026. 08. 14.
//

/// A provider-neutral client capable of delivering notifications to supported targets.
///
/// The target determines how the provider should route the notification. A
/// provider that does not support a target should throw
/// ``PushClientError/unsupportedTarget``.
public protocol PushClient: Sendable {
/// Sends a notification to the specified delivery target.
///
/// - Parameters:
/// - notification: The notification content and delivery options.
/// - target: A device token or provider-managed topic.
/// - Throws: A ``PushClientError`` when the notification cannot be sent.
func send(
notification: PushNotification,
to target: PushDeliveryTarget
) async throws(PushClientError)
}
33 changes: 33 additions & 0 deletions Sources/FeatherPush/Client/PushClientError.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
//
// PushClientError.swift
// feather-push
//
// Created by Binary Birds on 2026. 08. 14.
//

/// Provider-neutral errors that can occur while delivering a push notification.
public enum PushClientError: Error {

/// The topic is empty or is not valid for the provider.
case invalidTopic
/// The device token is empty or is not valid for the provider.
case invalidDeviceToken
/// The notification payload is invalid or contains unsupported values.
case invalidNotification
/// The provider does not support the requested target type.
case unsupportedTarget
/// The provider credentials are invalid or the request is unauthorized.
case unauthorized
/// The provider temporarily rejected the request because it was rate limited.
case rateLimited
/// The provider is temporarily unavailable; retrying may succeed later.
case unavailable
/// The provider rejected the request with a descriptive reason.
///
/// - Parameter reason: The provider-supplied rejection message.
case rejected(String)
/// An underlying error without a more specific provider-neutral mapping.
///
/// - Parameter error: The underlying provider or transport error.
case unknown(Error)
}
18 changes: 18 additions & 0 deletions Sources/FeatherPush/Client/PushDeliveryTarget.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
//
// PushDeliveryTarget.swift
// feather-push
//
// Created by Binary Birds on 2026. 08. 19.
//

/// A destination used when sending a push notification.
///
/// The meaning and availability of each target is provider-specific. A topic
/// may represent a group of subscribed devices for one provider, while another
/// provider may use its topic value only to identify an application.
public enum PushDeliveryTarget: Sendable, Equatable {
/// A provider-issued token identifying a single device.
case deviceToken(String)
/// A provider-managed topic identifying a destination group or service.
case topic(String)
}
6 changes: 3 additions & 3 deletions Sources/FeatherPush/Models/Delivery.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@
// Created by Binary Birds on 2026. 08. 13.
//

/// The delivery priority of a push notification.
/// The requested presentation mode for a push notification.
public enum Delivery: String, Sendable {
/// Delivers the notification normally.
/// Delivers a notification that may be presented to the user.
case normal
/// Delivers the notification without presenting an alert.
/// Delivers notification data without requesting a visible alert.
case silent
}
30 changes: 22 additions & 8 deletions Sources/FeatherPush/Models/PushNotification.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,28 +5,42 @@
// Created by Binary Birds on 2026. 08. 13.
//

/// A push notification payload.
/// The provider-neutral content and delivery options for a push notification.
public struct PushNotification: Sendable {
/// The notification title.
public let title: String
/// The notification body.
public let body: String
/// Provider-specific notification metadata.
/// Application-defined key-value data delivered with the notification.
///
/// Providers may impose restrictions on keys and values. Reserved provider
/// keys may be rejected by a concrete client.
public let data: [String: String]
/// The requested delivery priority.
/// Whether the notification should be presented or delivered silently.
public let delivery: Delivery
/// An optional deep link opened when the notification is selected.
/// An optional deep link for the application to open when selected.
public let deepLink: String?
/// An optional image URL supported by providers that render rich notifications.
/// An optional image URL for providers that support rich notifications.
public let imageURL: String?
/// An optional badge value for the application icon.
public let badge: Int?
/// An optional notification sound.
/// An optional sound for providers and platforms that support notification sounds.
public let sound: Sound?
/// An optional identifier used to collapse equivalent notifications.
/// An optional provider-specific identifier used to collapse equivalent notifications.
public let collapseID: String?

/// Creates a push notification.
/// Creates a provider-neutral push notification.
///
/// - Parameters:
/// - title: The notification title.
/// - body: The notification body.
/// - data: Application-defined data delivered with the notification.
/// - delivery: The requested presentation mode.
/// - deepLink: An optional application deep link.
/// - imageURL: An optional image URL for rich notifications.
/// - badge: An optional application badge value.
/// - sound: An optional notification sound.
/// - collapseID: An optional identifier for collapsing equivalent notifications.
public init(
title: String,
body: String,
Expand Down
4 changes: 2 additions & 2 deletions Sources/FeatherPush/Models/Sound.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@
// Created by Binary Birds on 2026. 08. 13.
//

/// The sound to play when a notification is presented.
/// A notification sound requested when a provider presents the notification.
public enum Sound: Sendable, Equatable {
/// Use the platform default notification sound.
case `default`
/// Use a bundled, provider-specific sound name.
/// Use a sound bundled with the target application.
case named(String)
}
21 changes: 0 additions & 21 deletions Sources/FeatherPush/PushClient.swift

This file was deleted.

25 changes: 0 additions & 25 deletions Sources/FeatherPush/PushClientError.swift

This file was deleted.

5 changes: 4 additions & 1 deletion Tests/FeatherPushTests/FeatherPushTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,16 @@ struct FeatherPushTests {
func clientErrorsExposeProviderNeutralCases() {
let errors: [PushClientError] = [
.invalidTopic,
.invalidDeviceToken,
.invalidNotification,
.unsupportedTarget,
.unauthorized,
.rateLimited,
.unavailable,
.rejected("provider rejected the request"),
]

#expect(errors.count == 6)
#expect(errors.count == 8)
}

}
Loading