Something went wrong. Try again.
This repository has no description
Something went wrong. Try again.
12 kB · 285 lines
Swift
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286import ComposableArchitecture2import Dependencies#if os(Android)import SkipFuse#elseif canImport(OSLog)import OSLog#endif
#if os(Android) || canImport(OSLog)private let logger = Logger( subsystem: "app.openmusicevent", category: "notifications")
private func logInfo(_ message: String) {#if os(Android) logger.info(message)#else logger.info("\(message, privacy: .public)")#endif}
private func logError(_ message: String) {#if os(Android) logger.error(message)#else logger.error("\(message, privacy: .public)")#endif}#endif
/// A transparent decorator that connects app-level notification transport to a content feature tree.////// The wrapper forwards provider-issued device destinations to the host's authenticated client/// without storing token material in feature state.////// Descendants publish desired subscriptions with ``FeatureProtocol/notificationSubscriptions(_:)``./// Provider-delivered notification-center events enter through an app-owned ``NotificationEvent``/// trigger. Transport signals and decoded content leave through their own app-owned triggers.public struct NotificationsFeature< Content: FeatureProtocol, Route: NotificationRoute>: FeatureProtocol { private let appDelegateEventTrigger: KeyPath<Content.State, Trigger<AppDelegateEvent>> private let content: Content private let contentTrigger: KeyPath<Content.State, Trigger<NotificationContent<Route>>> private let destinationTrigger: KeyPath<Content.State, Trigger<Push.Destination>> private let notificationEventTrigger: KeyPath<Content.State, Trigger<NotificationEvent>> private let signalTrigger: KeyPath<Content.State, Trigger<NotificationSignal>>
@Dependency(\.notificationsClient) private var notificationsClient
public init( appDelegateEventTrigger: KeyPath<Content.State, Trigger<AppDelegateEvent>>, contentTrigger: KeyPath<Content.State, Trigger<NotificationContent<Route>>>, destinationTrigger: KeyPath<Content.State, Trigger<Push.Destination>>, notificationEventTrigger: KeyPath<Content.State, Trigger<NotificationEvent>>, signalTrigger: KeyPath<Content.State, Trigger<NotificationSignal>>, content makeContent: @escaping @Sendable () -> Content ) { self.appDelegateEventTrigger = appDelegateEventTrigger self.contentTrigger = contentTrigger self.destinationTrigger = destinationTrigger self.notificationEventTrigger = notificationEventTrigger self.signalTrigger = signalTrigger self.content = makeContent() }
fileprivate init( appDelegateEventTrigger: KeyPath<Content.State, Trigger<AppDelegateEvent>>, contentTrigger: KeyPath<Content.State, Trigger<NotificationContent<Route>>>, destinationTrigger: KeyPath<Content.State, Trigger<Push.Destination>>, notificationEventTrigger: KeyPath<Content.State, Trigger<NotificationEvent>>, signalTrigger: KeyPath<Content.State, Trigger<NotificationSignal>>, content: Content ) { self.appDelegateEventTrigger = appDelegateEventTrigger self.contentTrigger = contentTrigger self.destinationTrigger = destinationTrigger self.notificationEventTrigger = notificationEventTrigger self.signalTrigger = signalTrigger self.content = content }
public typealias State = Content.State public typealias Action = Content.Action
@FeatureBindable private var store = FeatureStore<State, Action>()
public var body: some FeatureProtocol<State, Action> { content .onTrigger(store.state[keyPath: destinationTrigger]) { destination, _ in#if os(Android) || canImport(OSLog) let provider: String let applicationID: String switch destination { case .apns(let registration): provider = "APNs" applicationID = registration.applicationID.rawValue case .fcm(let registration): provider = "FCM" applicationID = registration.applicationID.rawValue } logInfo( "Registering \(provider) notification destination for \(applicationID)" )#endif store.addTask { do { try await notificationsClient.registerDestination(destination)#if os(Android) || canImport(OSLog) logInfo( "Registered \(provider) notification destination for \(applicationID)" )#endif } catch let error as CancellationError { throw error } catch {#if os(Android) || canImport(OSLog) logError( "Notification destination registration failed for \(applicationID): \(String(describing: error))" )#endif // The next resume or destination change retries the desired state. } } } .onTrigger(store.state[keyPath: appDelegateEventTrigger]) { event, _ in switch event { case .resumed:#if os(Android) || canImport(OSLog) logInfo("Synchronizing notification registration after application resumed")#endif store.addTask { do { try await notificationsClient.synchronize()#if os(Android) || canImport(OSLog) logInfo("Notification registration synchronization completed")#endif } catch let error as CancellationError { throw error } catch {#if os(Android) || canImport(OSLog) logError( "Notification registration synchronization failed: \(String(describing: error))" )#endif throw error } } default: break } } .onTrigger(store.state[keyPath: notificationEventTrigger]) { notification, _ in let message: NotificationMessage<Route> do { message = try NotificationProviderMessage(json: notification.json) .message(as: Route.self) } catch {#if os(Android) || canImport(OSLog) logError( "Ignoring invalid delivered notification JSON: \(String(describing: error))" )#endif return }#if os(Android) || canImport(OSLog) switch message { case .content(let content): logInfo( "Received \(String(describing: notification.delivery)) notification for route \(content.route.notificationRoute) from source \(content.source.id.rawValue) at version \(content.source.version.rawValue)" ) case .sourceAvailable(let source): logInfo( "Received \(String(describing: notification.delivery)) notification for source \(source.id.rawValue) at version \(source.version.rawValue)" ) }#endif switch message { case .content(let content): switch notification.delivery { case .userInteraction: store.addTask { let route = try store.state[keyPath: contentTrigger](content) let refresh = try store.state[keyPath: signalTrigger]( .sourceAvailable(content.source) ) await route?.value await refresh?.value#if os(Android) || canImport(OSLog) logInfo("Delivered notification content to the application feature tree")#endif } case .contentAvailable, .foreground: store.addTask { let trigger = try store.state[keyPath: signalTrigger]( .sourceAvailable(content.source) ) await trigger?.value#if os(Android) || canImport(OSLog) logInfo("Delivered notification signal to the application feature tree")#endif } } case .sourceAvailable(let source): store.addTask { let trigger = try store.state[keyPath: signalTrigger]( .sourceAvailable(source) ) await trigger?.value#if os(Android) || canImport(OSLog) logInfo("Delivered notification signal to the application feature tree")#endif } } } .onMount { _ in store.addTask { for await signal in await notificationsClient.signals() {#if os(Android) || canImport(OSLog) switch signal { case .sourceAvailable(let source): logInfo( "Received notification transport signal for source \(source.id.rawValue) at version \(source.version.rawValue)" ) }#endif let trigger = try store.state[keyPath: signalTrigger](signal) await trigger?.value#if os(Android) || canImport(OSLog) logInfo("Delivered notification transport signal to the application feature tree")#endif } } } .onPreferenceChange(NotificationSubscriptionsPreferenceKey.self) { subscriptions, _ in store.addTask { do { try await notificationsClient.replaceSubscriptions(subscriptions)#if os(Android) || canImport(OSLog) let sourceIDs = subscriptions .map(\.sourceID.rawValue) .sorted() .joined(separator: ", ") logInfo( "Replaced notification subscriptions; count: \(subscriptions.count); sources: \(sourceIDs)" )#endif } catch let error as CancellationError { throw error } catch {#if os(Android) || canImport(OSLog) logError( "Replacing notification subscriptions failed: \(String(describing: error))" )#endif throw error } } } }}
public extension FeatureProtocol { /// Installs notification orchestration at the owning app-root boundary. func notifications<Route: NotificationRoute>( appDelegateEventTrigger: KeyPath<State, Trigger<AppDelegateEvent>>, contentTrigger: KeyPath<State, Trigger<NotificationContent<Route>>>, destinationTrigger: KeyPath<State, Trigger<Push.Destination>>, notificationEventTrigger: KeyPath<State, Trigger<NotificationEvent>>, signalTrigger: KeyPath<State, Trigger<NotificationSignal>> ) -> some FeatureProtocol<State, Action> { NotificationsFeature<Self, Route>( appDelegateEventTrigger: appDelegateEventTrigger, contentTrigger: contentTrigger, destinationTrigger: destinationTrigger, notificationEventTrigger: notificationEventTrigger, signalTrigger: signalTrigger, content: self ) }}