Sync push
This commit is contained in:
@ -1,61 +1,18 @@
|
||||
import SwiftUI
|
||||
import APNSwift
|
||||
import Push
|
||||
import PushMessageDefinitions
|
||||
import SFSafeSymbols
|
||||
import UniformTypeIdentifiers
|
||||
|
||||
struct ContentView: View {
|
||||
|
||||
@AppStorage("pushToken")
|
||||
var pushToken: PushToken?
|
||||
var pushToken: String?
|
||||
|
||||
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
|
||||
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -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 {
|
||||
|
13
FlurSchnaps/TokenUpload.swift
Normal file
13
FlurSchnaps/TokenUpload.swift
Normal file
@ -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
|
||||
}
|
||||
}
|
@ -39,3 +39,5 @@
|
||||
"device-list-title" = "Geräte";
|
||||
|
||||
"show-device-list-button" = "Liste einzeigen";
|
||||
|
||||
"push-token-title" = "Geräte-Identifikator";
|
||||
|
@ -39,3 +39,5 @@
|
||||
"device-list-title" = "Devices";
|
||||
|
||||
"show-device-list-button" = "Show device list";
|
||||
|
||||
"push-token-title" = "Push token";
|
||||
|
Reference in New Issue
Block a user