From 1c70b21e252ab7eab68af00862f36a1d4d6c8d2e Mon Sep 17 00:00:00 2001 From: Christoph Hagen Date: Tue, 27 Sep 2022 21:01:22 +0200 Subject: [PATCH] Sync push --- .gitignore | 2 + FlurSchnaps.xcodeproj/project.pbxproj | 42 +-- .../xcshareddata/swiftpm/Package.resolved | 80 +----- .../UserInterfaceState.xcuserstate | Bin 0 -> 43382 bytes FlurSchnaps/ContentView.swift | 252 +++--------------- FlurSchnaps/DeviceList.swift | 91 ------- FlurSchnaps/FlurSchnapsApp.swift | 50 +++- FlurSchnaps/TokenUpload.swift | 13 + FlurSchnaps/de.lproj/Localizable.strings | 2 + FlurSchnaps/en.lproj/Localizable.strings | 2 + 10 files changed, 125 insertions(+), 409 deletions(-) create mode 100644 .gitignore create mode 100644 FlurSchnaps.xcodeproj/project.xcworkspace/xcuserdata/imac.xcuserdatad/UserInterfaceState.xcuserstate delete mode 100644 FlurSchnaps/DeviceList.swift create mode 100644 FlurSchnaps/TokenUpload.swift diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9bea433 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ + +.DS_Store diff --git a/FlurSchnaps.xcodeproj/project.pbxproj b/FlurSchnaps.xcodeproj/project.pbxproj index faadb44..69498bc 100644 --- a/FlurSchnaps.xcodeproj/project.pbxproj +++ b/FlurSchnaps.xcodeproj/project.pbxproj @@ -8,11 +8,11 @@ /* Begin PBXBuildFile section */ 881E0B26284B74E200435EC2 /* Data+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 881E0B25284B74E200435EC2 /* Data+Extensions.swift */; }; - 88DBE72A284B989C00D1573B /* DeviceList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88DBE729284B989C00D1573B /* DeviceList.swift */; }; + 88F92BA928C67EEB0078BEC4 /* TokenUpload.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88F92BA828C67EEB0078BEC4 /* TokenUpload.swift */; }; + 88F92BAC28C67F8B0078BEC4 /* BinaryCodable in Frameworks */ = {isa = PBXBuildFile; productRef = 88F92BAB28C67F8B0078BEC4 /* BinaryCodable */; }; E234995C284E1D02002B55F8 /* SFSafeSymbols in Frameworks */ = {isa = PBXBuildFile; productRef = E234995B284E1D02002B55F8 /* SFSafeSymbols */; }; E234995F284E372B002B55F8 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = E2349961284E372B002B55F8 /* Localizable.strings */; }; E2349964284F3133002B55F8 /* TextEntryField.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2349963284F3133002B55F8 /* TextEntryField.swift */; }; - E2349968284F78E3002B55F8 /* Push in Frameworks */ = {isa = PBXBuildFile; productRef = E2349967284F78E3002B55F8 /* Push */; }; E29A7E47284B6143000B908A /* FlurSchnapsApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29A7E46284B6143000B908A /* FlurSchnapsApp.swift */; }; E29A7E49284B6143000B908A /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29A7E48284B6143000B908A /* ContentView.swift */; }; E29A7E4B284B6144000B908A /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E29A7E4A284B6144000B908A /* Assets.xcassets */; }; @@ -23,7 +23,7 @@ 881E0B25284B74E200435EC2 /* Data+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+Extensions.swift"; sourceTree = ""; }; 88DBE727284B7EB200D1573B /* FlurSchnaps.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = FlurSchnaps.entitlements; sourceTree = ""; }; 88DBE728284B813500D1573B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; - 88DBE729284B989C00D1573B /* DeviceList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceList.swift; sourceTree = ""; }; + 88F92BA828C67EEB0078BEC4 /* TokenUpload.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TokenUpload.swift; sourceTree = ""; }; E2349960284E372B002B55F8 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; E2349962284E3733002B55F8 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = ""; }; E2349963284F3133002B55F8 /* TextEntryField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextEntryField.swift; sourceTree = ""; }; @@ -39,7 +39,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - E2349968284F78E3002B55F8 /* Push in Frameworks */, + 88F92BAC28C67F8B0078BEC4 /* BinaryCodable in Frameworks */, E234995C284E1D02002B55F8 /* SFSafeSymbols in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -72,10 +72,10 @@ E29A7E46284B6143000B908A /* FlurSchnapsApp.swift */, E29A7E48284B6143000B908A /* ContentView.swift */, E2349963284F3133002B55F8 /* TextEntryField.swift */, - 88DBE729284B989C00D1573B /* DeviceList.swift */, 881E0B25284B74E200435EC2 /* Data+Extensions.swift */, E29A7E4A284B6144000B908A /* Assets.xcassets */, E29A7E4C284B6144000B908A /* Preview Content */, + 88F92BA828C67EEB0078BEC4 /* TokenUpload.swift */, ); path = FlurSchnaps; sourceTree = ""; @@ -106,7 +106,7 @@ name = FlurSchnaps; packageProductDependencies = ( E234995B284E1D02002B55F8 /* SFSafeSymbols */, - E2349967284F78E3002B55F8 /* Push */, + 88F92BAB28C67F8B0078BEC4 /* BinaryCodable */, ); productName = FlurSchnaps; productReference = E29A7E43284B6143000B908A /* FlurSchnaps.app */; @@ -139,7 +139,7 @@ mainGroup = E29A7E3A284B6143000B908A; packageReferences = ( E234995A284E1D02002B55F8 /* XCRemoteSwiftPackageReference "SFSafeSymbols" */, - E2349966284F78E3002B55F8 /* XCRemoteSwiftPackageReference "Push-iOS" */, + 88F92BAA28C67F8B0078BEC4 /* XCRemoteSwiftPackageReference "BinaryCodable" */, ); productRefGroup = E29A7E44284B6143000B908A /* Products */; projectDirPath = ""; @@ -171,8 +171,8 @@ E2349964284F3133002B55F8 /* TextEntryField.swift in Sources */, E29A7E49284B6143000B908A /* ContentView.swift in Sources */, 881E0B26284B74E200435EC2 /* Data+Extensions.swift in Sources */, - 88DBE72A284B989C00D1573B /* DeviceList.swift in Sources */, E29A7E47284B6143000B908A /* FlurSchnapsApp.swift in Sources */, + 88F92BA928C67EEB0078BEC4 /* TokenUpload.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -395,6 +395,14 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ + 88F92BAA28C67F8B0078BEC4 /* XCRemoteSwiftPackageReference "BinaryCodable" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/christophhagen/BinaryCodable"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.0.0; + }; + }; E234995A284E1D02002B55F8 /* XCRemoteSwiftPackageReference "SFSafeSymbols" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/SFSafeSymbols/SFSafeSymbols"; @@ -403,27 +411,19 @@ minimumVersion = 3.0.0; }; }; - E2349966284F78E3002B55F8 /* XCRemoteSwiftPackageReference "Push-iOS" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://christophhagen.de/git/ch/Push-iOS"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 0.2.0; - }; - }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ + 88F92BAB28C67F8B0078BEC4 /* BinaryCodable */ = { + isa = XCSwiftPackageProductDependency; + package = 88F92BAA28C67F8B0078BEC4 /* XCRemoteSwiftPackageReference "BinaryCodable" */; + productName = BinaryCodable; + }; E234995B284E1D02002B55F8 /* SFSafeSymbols */ = { isa = XCSwiftPackageProductDependency; package = E234995A284E1D02002B55F8 /* XCRemoteSwiftPackageReference "SFSafeSymbols" */; productName = SFSafeSymbols; }; - E2349967284F78E3002B55F8 /* Push */ = { - isa = XCSwiftPackageProductDependency; - package = E2349966284F78E3002B55F8 /* XCRemoteSwiftPackageReference "Push-iOS" */; - productName = Push; - }; /* End XCSwiftPackageProductDependency section */ }; rootObject = E29A7E3B284B6143000B908A /* Project object */; diff --git a/FlurSchnaps.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/FlurSchnaps.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 4ef0330..2d85b9f 100644 --- a/FlurSchnaps.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/FlurSchnaps.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,39 +1,12 @@ { "pins" : [ { - "identity" : "apnswift", + "identity" : "binarycodable", "kind" : "remoteSourceControl", - "location" : "https://github.com/swift-server-community/APNSwift.git", + "location" : "https://github.com/christophhagen/BinaryCodable", "state" : { - "revision" : "99a3c7bb5fd211009438fb386d18c94bb2f63b17", - "version" : "4.0.0" - } - }, - { - "identity" : "jwt-kit", - "kind" : "remoteSourceControl", - "location" : "https://github.com/vapor/jwt-kit.git", - "state" : { - "revision" : "3537dd319dfbcc403a5165d8c19c4834e8e64730", - "version" : "4.5.0" - } - }, - { - "identity" : "push-definitions", - "kind" : "remoteSourceControl", - "location" : "https://christophhagen.de/git/ch/Push-Definitions.git", - "state" : { - "revision" : "3a4e93889b0cb5f500eeccd7ba2a099d0f3862f0", - "version" : "1.0.1" - } - }, - { - "identity" : "push-ios", - "kind" : "remoteSourceControl", - "location" : "https://christophhagen.de/git/ch/Push-iOS", - "state" : { - "revision" : "1131cb17c6114a0b41263be1b5788188548adc41", - "version" : "0.3.0" + "revision" : "2049887d460c0101e69922271a54a567dbf31e2c", + "version" : "1.2.2" } }, { @@ -44,51 +17,6 @@ "revision" : "c8c33d947d8a1c883aa19fd24e14fd738b06e369", "version" : "3.3.2" } - }, - { - "identity" : "swift-crypto", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-crypto.git", - "state" : { - "revision" : "d9825fa541df64b1a7b182178d61b9a82730d01f", - "version" : "2.1.0" - } - }, - { - "identity" : "swift-log", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-log.git", - "state" : { - "revision" : "5d66f7ba25daf4f94100e7022febf3c75e37a6c7", - "version" : "1.4.2" - } - }, - { - "identity" : "swift-nio", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-nio.git", - "state" : { - "revision" : "124119f0bb12384cef35aa041d7c3a686108722d", - "version" : "2.40.0" - } - }, - { - "identity" : "swift-nio-http2", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-nio-http2.git", - "state" : { - "revision" : "72bcaf607b40d7c51044f65b0f5ed8581a911832", - "version" : "1.21.0" - } - }, - { - "identity" : "swift-nio-ssl", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-nio-ssl.git", - "state" : { - "revision" : "1750873bce84b4129b5303655cce2c3d35b9ed3a", - "version" : "2.19.0" - } } ], "version" : 2 diff --git a/FlurSchnaps.xcodeproj/project.xcworkspace/xcuserdata/imac.xcuserdatad/UserInterfaceState.xcuserstate b/FlurSchnaps.xcodeproj/project.xcworkspace/xcuserdata/imac.xcuserdatad/UserInterfaceState.xcuserstate new file mode 100644 index 0000000000000000000000000000000000000000..094c37bdbea7c6686ed5785f752004a683963195 GIT binary patch literal 43382 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 1) - } func statusView(_ state: Bool?) -> some View { let symbol: SFSymbol @@ -95,6 +52,16 @@ struct ContentView: View { } } + @State + private var didTapText = false + + var tapFootnote: String { + if didTapText { + return "Copied to clipboard" + } + return "Double tap to copy token" + } + var body: some View { NavigationView { VStack(spacing: 8) { @@ -106,21 +73,23 @@ struct ContentView: View { 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 { + Spacer() + if let token = pushToken { + Text("push-token-title") + Text(token) + .font(.body.monospaced()) + .padding() + .onTapGesture(count: 2, perform: didDoubleTapToken) + Text(tapFootnote) + .font(.footnote) + } else { Text("register-for-remote-notifications-text") .padding() .multilineTextAlignment(.center) Button("register-for-remote-notifications-button", action: registerForRemoteNotifications) .padding() - } else if hasNotificationPermissions == nil { + } + if hasNotificationPermissions == nil { Button("request-notification-permission-button", action: requestNotificationPermission) .padding() } else if hasNotificationPermissions == false { @@ -129,70 +98,26 @@ struct ContentView: View { .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 { + .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() } } + + private func didDoubleTapToken() { + UIPasteboard.general + .setValue(pushToken!, forPasteboardType: UTType.plainText.identifier) + didTapText = true + DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(3)) { + self.didTapText = false + } + } @State private var timer: Timer? @@ -211,14 +136,9 @@ struct ContentView: View { timer?.invalidate() timer = nil } - + private func updateState() { updateNotificationPermissionState() - if isConfirmed { - updateDeviceList() - } else if couldBeRegistered { - checkPushRegistrationStatus() - } } func registerForRemoteNotifications() { @@ -239,108 +159,6 @@ struct ContentView: View { 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 { diff --git a/FlurSchnaps/DeviceList.swift b/FlurSchnaps/DeviceList.swift deleted file mode 100644 index c702a5c..0000000 --- a/FlurSchnaps/DeviceList.swift +++ /dev/null @@ -1,91 +0,0 @@ -import SwiftUI -import PushMessageDefinitions -import Push - -extension String { - - var nonEmpty: String? { - isEmpty ? nil : self - } -} - -struct DeviceList: View { - - let pushToken: PushToken - - let authToken: AuthenticationToken - - let api: PushClient - - @Binding - var isPresented: Bool - - @State - var devices: [DeviceRegistration] - - var body: some View { - NavigationView { - VStack(spacing: 0) { - List(devices) { device in - HStack { - Text(device.name.nonEmpty ?? "Device") - .font(.headline) - Spacer() - VStack(alignment: .leading) { - Text(device.application) - .font(.footnote) - .fontWeight(.bold) - .padding(.bottom, 2) - Text(device.pushToken.prefix(5).hexEncoded + "...") - .font(.caption) - } - } - }.refreshable { - await updateList() - } - }.toolbar { - ToolbarItem(placement: .navigationBarTrailing) { - Button(action: dismiss) { - Text("Cancel") - } - } - }.navigationBarTitle("device-list-title") - }.onAppear() { - Task { - await updateList() - } - } - } - - private func updateList() async { - let devices = await api.getDeviceList(pushToken: pushToken, authToken: authToken) - print("Updated device list: \(devices?.count ?? -1)") - DispatchQueue.main.async { - self.devices = devices ?? [] - } - } - - private func dismiss() { - isPresented = false - } -} - -struct DeviceList_Previews: PreviewProvider { - static var previews: some View { - DeviceList(pushToken: Data(repeating: 42, count: 32), - authToken: Data(repeating: 42, count: 16), - api: .init(server: URL(string: "https://christophhagen.de/push")!, application: "some"), - isPresented: .constant(true), - devices: [DeviceRegistration( - pushToken: Data([1,2,3,4,5]), - application: "CC Messenger", - name: "Some")]) - } -} - -extension DeviceRegistration: Identifiable { - - public var id: String { - pushToken.prefix(5).hexEncoded - } -} diff --git a/FlurSchnaps/FlurSchnapsApp.swift b/FlurSchnaps/FlurSchnapsApp.swift index 63369b8..11c7869 100644 --- a/FlurSchnaps/FlurSchnapsApp.swift +++ b/FlurSchnaps/FlurSchnapsApp.swift @@ -1,4 +1,5 @@ import SwiftUI +import BinaryCodable @main struct FlurSchnapsApp: App { @@ -15,12 +16,11 @@ struct FlurSchnapsApp: App { class AppDelegate: NSObject, UIApplicationDelegate { - @AppStorage("pushToken") + @AppStorage("deviceToken") var pushToken: Data? func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { - // For iOS 10 display notification (sent via APNS) UNUserNotificationCenter.current().delegate = self @@ -31,8 +31,9 @@ class AppDelegate: NSObject, UIApplicationDelegate { func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { - print("Registered with token: \(deviceToken)") - self.pushToken = deviceToken + let token = deviceToken.hexEncoded + print("Registered for remote notifications with token: \(token)") + uploadNewDeviceToken(deviceToken) } func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any], @@ -42,6 +43,47 @@ class AppDelegate: NSObject, UIApplicationDelegate { completionHandler(UIBackgroundFetchResult.newData) } + + private func uploadNewDeviceToken(_ token: Data) { + if token == pushToken { + // Device token unchanged + return + } + + Task { + let tokenUpload = TokenUpload(currentToken: token, previousToken: pushToken) + let data: Data + do { + data = try BinaryEncoder().encode(tokenUpload) + + } catch { + print("Failed to encode token upload") + return + } + let result: URLResponse + do { + var request = URLRequest(url: URL(string: "https://christophhagen.de/schnaps/token")!) + request.httpMethod = "POST" + (_, result) = try await URLSession.shared.upload(for: request, from: data) + } catch { + print("Failed to upload token: \(error)") + return + } + guard let response = result as? HTTPURLResponse else { + print("Invalid response \(result)") + return + } + guard response.statusCode == 200 else { + print("Invalid response to token upload: \(response.statusCode)") + return + } + print("Push token uploaded") + DispatchQueue.main.async { + self.pushToken = token + } + } + // Upload new token, possibly with old one + } } extension AppDelegate: UNUserNotificationCenterDelegate { diff --git a/FlurSchnaps/TokenUpload.swift b/FlurSchnaps/TokenUpload.swift new file mode 100644 index 0000000..67a2115 --- /dev/null +++ b/FlurSchnaps/TokenUpload.swift @@ -0,0 +1,13 @@ +import Foundation + +struct TokenUpload: Codable { + + let currentToken: Data + + let previousToken: Data? + + enum CodingKeys: Int, CodingKey { + case currentToken = 1 + case previousToken = 2 + } +} diff --git a/FlurSchnaps/de.lproj/Localizable.strings b/FlurSchnaps/de.lproj/Localizable.strings index 3a2a549..62fb173 100644 --- a/FlurSchnaps/de.lproj/Localizable.strings +++ b/FlurSchnaps/de.lproj/Localizable.strings @@ -39,3 +39,5 @@ "device-list-title" = "Geräte"; "show-device-list-button" = "Liste einzeigen"; + +"push-token-title" = "Geräte-Identifikator"; diff --git a/FlurSchnaps/en.lproj/Localizable.strings b/FlurSchnaps/en.lproj/Localizable.strings index 82b328b..6e00f09 100644 --- a/FlurSchnaps/en.lproj/Localizable.strings +++ b/FlurSchnaps/en.lproj/Localizable.strings @@ -39,3 +39,5 @@ "device-list-title" = "Devices"; "show-device-list-button" = "Show device list"; + +"push-token-title" = "Push token";