// // NotificationsPrefsView.swift // Tusker // // Created by Shadowfacts on 4/6/24. // Copyright © 2024 Shadowfacts. All rights reserved. // import SwiftUI import UserNotifications import UserAccounts import PushNotifications import TuskerComponents struct NotificationsPrefsView: View { @State private var error: NotificationsSetupError? @State private var isSetup = AsyncToggle.Mode.off @State private var pushProxyRegistration: PushProxyRegistration? @ObservedObject private var userAccounts = UserAccountsManager.shared var body: some View { List { enableSection if isSetup == .on, let pushProxyRegistration { accountsSection(pushProxyRegistration: pushProxyRegistration) } } .listStyle(.insetGrouped) .appGroupedListBackground(container: PreferencesNavigationController.self) .navigationTitle("Notifications") } private var enableSection: some View { Section { AsyncToggle("Push Notifications", mode: $isSetup, onChange: isSetupChanged(newValue:)) } .appGroupedListRowBackground() .alertWithData("An Error Occurred", data: $error) { error in Button("OK") {} } message: { error in Text(error.localizedDescription) } .task { @MainActor in pushProxyRegistration = PushManager.shared.proxyRegistration isSetup = pushProxyRegistration != nil ? .on : .off if !UIApplication.shared.isRegisteredForRemoteNotifications { _ = await registerForRemoteNotifications() } } } private func accountsSection(pushProxyRegistration: PushProxyRegistration) -> some View { Section { ForEach(userAccounts.accounts) { account in PushInstanceSettingsView(account: account, pushProxyRegistration: pushProxyRegistration) } } .appGroupedListRowBackground() } private func isSetupChanged(newValue: Bool) async -> Bool { if newValue { return await startRegistration() } else { return await unregister() } } private func startRegistration() async -> Bool { let authorized: Bool do { // TODO: support .providesAppNotificationSettings authorized = try await UNUserNotificationCenter.current().requestAuthorization(options: [.alert]) } catch { self.error = .requestingAuthorization(error) return false } guard authorized else { return false } return await registerForRemoteNotifications() } private func registerForRemoteNotifications() async -> Bool { do { pushProxyRegistration = try await PushManager.shared.register() return true } catch { self.error = .registering(error) return false } } private func unregister() async -> Bool { do { try await PushManager.shared.unregister() pushProxyRegistration = nil for subscription in PushManager.shared.subscriptions { if let account = UserAccountsManager.shared.getAccount(id: subscription.accountID) { let mastodonController = MastodonController.getForAccount(account) do { try await mastodonController.deletePushSubscription() PushManager.shared.removeSubscription(account: account) PushManager.logger.debug("Push subscription removed on \(account.instanceURL)") // this is a bit of a hack. the PushInstanceSettingsViews need to know to update // their @State variables after we remove the subscription NotificationCenter.default.post(name: .pushSubscriptionRemoved, object: account.id) } catch { PushManager.logger.error("Erroring removing push subscription: \(String(describing: error))") } } } return true } catch { self.error = .unregistering(error) return false } } } private enum NotificationsSetupError: LocalizedError { case requestingAuthorization(any Error) case registering(any Error) case unregistering(any Error) var errorDescription: String? { switch self { case .requestingAuthorization(let error): "Notifications authorization request failed: \(error.localizedDescription)" case .registering(let error): "Registration failed: \(error.localizedDescription)" case .unregistering(let error): "Deactivation failed: \(error.localizedDescription)" } } } extension Notification.Name { static let pushSubscriptionRemoved = Notification.Name("Tusker.pushSubscriptionRemoved") }