import SwiftUI import APNSwift import Push import PushMessageDefinitions import SFSafeSymbols struct ContentView: View { @AppStorage("pushToken") var pushToken: PushToken? var hasPushToken: Bool { pushToken != nil } @AppStorage("authToken") var authToken: AuthenticationToken? @State var isConfirmed = false @State var hasNotificationPermissions: Bool? = nil @State var showDeviceList = false @State var api: PushClient? @AppStorage("server") var server: String = "" @AppStorage("application") var application = "" @AppStorage("deviceName") var deviceName: String = "" @State var deviceList: [DeviceRegistration] = [] var couldBeRegistered: Bool { pushToken != nil && authToken != nil } @AppStorage("pushTitle") var pushMessageTitle: String = "" @AppStorage("pushBody") var pushMessageText: String = "" @State var includeOwnDeviceInPush = false var canSendNotification: Bool { isConfirmed && (includeOwnDeviceInPush || deviceList.count > 1) } func statusView(_ state: Bool?) -> some View { let symbol: SFSymbol let color: Color if let state = state { symbol = state ? .checkmarkCircle : .xmarkCircle color = state ? .green : .red } else { symbol = .questionmarkCircle color = .gray } return Image(systemSymbol: symbol) .renderingMode(.template) .foregroundColor(color) } func updateNotificationPermissionState() { Task { let state = await getPushPermissionState() DispatchQueue.main.async { hasNotificationPermissions = state } } } private func getPushPermissionState() async -> Bool? { let settings = await UNUserNotificationCenter.current().notificationSettings() switch settings.authorizationStatus { case .authorized, .provisional, .ephemeral: return true case .denied: return false case .notDetermined: return nil @unknown default: return nil } } var body: some View { NavigationView { VStack(spacing: 8) { HStack { statusView(hasPushToken) Text("remote-notifications-title") } HStack { statusView(hasNotificationPermissions) Text("notification-permissions-title") } HStack { statusView(authToken != nil) Text("push-server-registration-title") } HStack { statusView(deviceList.count > 1) Text("other-devices-title") } if pushToken == nil { Text("register-for-remote-notifications-text") .padding() .multilineTextAlignment(.center) Button("register-for-remote-notifications-button", action: registerForRemoteNotifications) .padding() } else if hasNotificationPermissions == nil { Button("request-notification-permission-button", action: requestNotificationPermission) .padding() } else if hasNotificationPermissions == false { Text("no-notification-permissions-text") .padding() .multilineTextAlignment(.center) Button("no-notification-permissions-button", action: openNotificationSettings) .padding() } else if authToken == nil { Text("register-device-text") .padding() TextEntryField("Server url", placeholder: "register-device-server-placeholder", symbol: .network, showClearButton: true, text: $server) .padding(.horizontal, 50) .padding(.top) TextEntryField("Application", placeholder: "register-device-application-placeholder", symbol: .questionmarkApp, showClearButton: true, text: $application) .padding(.horizontal, 50) .padding(.top) TextEntryField("Device name", placeholder: "register-device-name-placeholder", symbol: .iphone, text: $deviceName) .padding(.horizontal, 50) .padding(.top) Button("register-device-button", action: register) .disabled(pushToken == nil || authToken != nil || deviceName.isEmpty) .padding() } else { Text("push-message-description") .padding() TextEntryField("Push title", placeholder: "push-message-title-placeholder", symbol: .bubbleLeft, showClearButton: true, text: $pushMessageTitle) .padding(.horizontal, 50) .disabled(!isConfirmed) TextEntryField("Push text", placeholder: "push-message-placeholder", symbol: .textformat, showClearButton: true, text: $pushMessageText) .padding(.horizontal, 50) .disabled(!isConfirmed) Toggle("toggle-include-own-device-text", isOn: $includeOwnDeviceInPush) .padding(.horizontal, 50) .padding(.top) Button("send-notification-button", action: sendPush) .disabled(!canSendNotification) .padding() Button("show-device-list-button", action: showDevices) .disabled(!isConfirmed) .padding() } Spacer() } .navigationTitle("FlurSchnaps") } .sheet(isPresented: $showDeviceList) { if let push = pushToken, let auth = authToken, let api = api { DeviceList(pushToken: push, authToken: auth, api: api, isPresented: $showDeviceList, devices: deviceList) } }.onAppear { startPeriodicUpdates() if server == "" { server = "https://christophhagen.de/push" } if application == "" { application = "FlurSchnaps" } if pushMessageTitle == "" { pushMessageTitle = "Flur-Schnaps" } if pushMessageText == "" { pushMessageText = "Du hast 30 Sekunden, um im Flur zu erscheinen" } }.onDisappear { stopPeriodicUpdates() } } @State private var timer: Timer? private func startPeriodicUpdates() { guard timer == nil else { return } timer = Timer.scheduledTimer(withTimeInterval: 5, repeats: true) { _ in updateState() } updateState() } private func stopPeriodicUpdates() { timer?.invalidate() timer = nil } private func updateState() { updateNotificationPermissionState() if isConfirmed { updateDeviceList() } else if couldBeRegistered { checkPushRegistrationStatus() } } func registerForRemoteNotifications() { UIApplication.shared.registerForRemoteNotifications() } func requestNotificationPermission() { let authOptions: UNAuthorizationOptions = [.alert, .badge, .sound] UNUserNotificationCenter.current().requestAuthorization( options: authOptions, completionHandler: {_, _ in updateNotificationPermissionState() }) } func openNotificationSettings() { if let appSettings = URL(string: UIApplication.openSettingsURLString), UIApplication.shared.canOpenURL(appSettings) { UIApplication.shared.open(appSettings) } } func register() { guard let token = pushToken else { print("No token to register") return } guard let url = URL(string: server) else { return } let api = PushClient(server: url, application: application) self.api = api let name = deviceName Task { print("Registering...") guard let auth = await api.register(token: token, name: name) else { DispatchQueue.main.async { authToken = nil isConfirmed = false } return } print("Registered") DispatchQueue.main.async { authToken = auth isConfirmed = false updateDeviceList() } } } func checkPushRegistrationStatus() { guard let token = pushToken, let authToken = authToken, let api = api else { return } Task { let confirmed = await api.isConfirmed(token: token, authentication: authToken) if !confirmed { print(token.base64EncodedString()) print(authToken.base64EncodedString()) } DispatchQueue.main.async { isConfirmed = confirmed } } } func updateDeviceList() { guard let authToken = authToken, let pushToken = pushToken, let api = api else { return } Task { let devices = await api.getDeviceList(pushToken: pushToken, authToken: authToken) DispatchQueue.main.async { self.deviceList = devices ?? [] } } } func showDevices() { showDeviceList = true } func sendPush() { guard let authToken = authToken, let pushToken = pushToken, let api = api else { return } var recipients = deviceList.map { $0.pushToken } guard recipients.count > 0 else { return } if !includeOwnDeviceInPush { recipients = recipients.filter { $0 != pushToken } } let body = pushMessageText let alert = APNSwiftAlert( title: pushMessageTitle, body: body) let payload = APNSwiftPayload( alert: alert, sound: .normal("default")) let content = PushMessage( recipients: recipients, payload: payload, pushType: .alert) let sender = DeviceAuthentication( pushToken: pushToken, authentication: authToken) let message = AuthenticatedPushMessage( sender: sender, message: content) Task { let sent = await api.send(push: message) print("Sent push message: \(sent)") } } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() .previewDevice("iPhone 8") } }