// // NotificationsPrefsView.swift // Tusker // // Created by Shadowfacts on 4/6/24. // Copyright © 2024 Shadowfacts. All rights reserved. // import SwiftUI import UserNotifications import UserAccounts struct NotificationsPrefsView: View { @State private var error: NotificationsSetupError? @State private var isSetup = TriStateToggle.Mode.off @State private var pushProxyRegistration: PushProxyRegistration? @ObservedObject private var userAccounts = UserAccountsManager.shared var body: some View { List { enableSection if isSetup == .on { accountsSection } } .listStyle(.insetGrouped) .appGroupedListBackground(container: PreferencesNavigationController.self) .navigationTitle("Notifications") } private var enableSection: some View { Section { HStack { Text("Push Notifications") Spacer() TriStateToggle(titleKey: "Push Notifications Enabled", 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.pushProxyRegistration isSetup = pushProxyRegistration != nil ? .on : .off if !UIApplication.shared.isRegisteredForRemoteNotifications { _ = await registerForRemoteNotifications() } } } private var accountsSection: some View { Section { ForEach(userAccounts.accounts) { account in PrefsAccountView(account: account) } } .appGroupedListRowBackground() } private func isSetupChanged(newValue: Bool) async { if newValue { let success = await startRegistration() if !success { isSetup = .off } } else { let success = await unregister() if !success { isSetup = .on } } } private func startRegistration() async -> Bool { let authorized: Bool do { 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(transactionID: 0) return true } catch { self.error = .registering(error) return false } } private func unregister() async -> Bool { do { try await PushManager.shared.unregister() pushProxyRegistration = nil return true } catch { self.error = .unregistering(error) return false } } } private struct TriStateToggle: View { let titleKey: LocalizedStringKey @Binding var mode: Mode let onChange: (Bool) async -> Void @State private var isOn: Bool = false var body: some View { toggleOrSpinner .onChange(of: mode) { newValue in switch newValue { case .off: isOn = false case .loading: break case .on: isOn = true } } } @ViewBuilder private var toggleOrSpinner: some View { if mode == .loading { ProgressView() } else { Toggle(titleKey, isOn: Binding { isOn } set: { newValue in isOn = newValue mode = .loading Task { await onChange(newValue) mode = newValue ? .on : .off } }) .labelsHidden() } } enum Mode { case off case loading case on } } 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)" } } }