Sync push

This commit is contained in:
Christoph Hagen
2022-09-27 21:01:22 +02:00
parent 1a459672d9
commit 1c70b21e25
10 changed files with 125 additions and 409 deletions

View File

@ -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 {

View File

@ -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
}
}

View File

@ -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 {

View 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
}
}

View File

@ -39,3 +39,5 @@
"device-list-title" = "Geräte";
"show-device-list-button" = "Liste einzeigen";
"push-token-title" = "Geräte-Identifikator";

View File

@ -39,3 +39,5 @@
"device-list-title" = "Devices";
"show-device-list-button" = "Show device list";
"push-token-title" = "Push token";