First working version
This commit is contained in:
131
FlurSchnaps/API.swift
Normal file
131
FlurSchnaps/API.swift
Normal file
@ -0,0 +1,131 @@
|
||||
import Foundation
|
||||
import CryptoKit
|
||||
import PushAPI
|
||||
import SwiftUI
|
||||
|
||||
final class API {
|
||||
|
||||
@AppStorage("server")
|
||||
var server: String = ""
|
||||
|
||||
var url: URL? {
|
||||
URL(string: server)
|
||||
}
|
||||
|
||||
init() {
|
||||
}
|
||||
|
||||
init(server: URL) {
|
||||
self.server = server.path
|
||||
}
|
||||
|
||||
init(server: URL, application: ApplicationId) {
|
||||
self.server = server.path
|
||||
self.application = application
|
||||
}
|
||||
|
||||
@AppStorage("application")
|
||||
var application: ApplicationId = ""
|
||||
|
||||
private static let encoder = JSONEncoder()
|
||||
private static let decoder = JSONDecoder()
|
||||
|
||||
func register(token: PushToken, name: String) async -> AuthenticationToken? {
|
||||
let device = DeviceRegistration(
|
||||
pushToken: token,
|
||||
application: application,
|
||||
name: name)
|
||||
guard let token = await post(.registerNewDevice, body: device) else {
|
||||
print("Failed to register")
|
||||
return nil
|
||||
}
|
||||
guard token.count == 16 else {
|
||||
print("Failed to register: Unexpected token length: \(token.count)")
|
||||
return nil
|
||||
}
|
||||
return token
|
||||
}
|
||||
|
||||
func getDeviceList(pushToken: PushToken, authToken: AuthenticationToken) async -> [DeviceRegistration] {
|
||||
let device = DeviceAuthentication(pushToken: pushToken, authentication: authToken)
|
||||
guard let data = await post(.listDevicesInApplication, body: device) else {
|
||||
print("Devices: Failed")
|
||||
return []
|
||||
}
|
||||
do {
|
||||
return try API.decoder.decode([DeviceRegistration].self, from: data)
|
||||
} catch {
|
||||
print("Devices: Failed to decode response")
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
func getUnconfirmedDevices(masterKey: String) async -> [DeviceRegistration] {
|
||||
let hash = hash(masterKey)
|
||||
guard let data = await post(.listUnapprovedDevices, bodyData: hash) else {
|
||||
print("Devices: Failed")
|
||||
return []
|
||||
}
|
||||
do {
|
||||
return try API.decoder.decode([DeviceRegistration].self, from: data)
|
||||
} catch {
|
||||
print("Devices: Failed to decode response")
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
private func hash(_ masterKey: String) -> Data {
|
||||
Data(SHA256.hash(data: masterKey.data(using: .utf8)!))
|
||||
}
|
||||
|
||||
func confirm(pushToken: PushToken, with masterKey: String) async -> Bool {
|
||||
let hash = hash(masterKey)
|
||||
let device = DeviceDecision(pushToken: pushToken, masterKeyHash: hash)
|
||||
return await post(.approveDevice, body: device) != nil
|
||||
}
|
||||
|
||||
func reject(pushToken: PushToken, with masterKey: String) async -> Bool {
|
||||
let hash = hash(masterKey)
|
||||
let device = DeviceDecision(pushToken: pushToken, masterKeyHash: hash)
|
||||
return await post(.rejectDevice, body: device) != nil
|
||||
}
|
||||
|
||||
func isConfirmed(token: PushToken, authentication: AuthenticationToken) async -> Bool {
|
||||
let device = DeviceAuthentication(pushToken: token, authentication: authentication)
|
||||
return await post(.isDeviceApproved, body: device) != nil
|
||||
}
|
||||
|
||||
func send(push: AuthenticatedPushMessage) async -> Bool {
|
||||
await post(.sendPushNotification, body: push) != nil
|
||||
}
|
||||
|
||||
private func post<T>(_ route: Route, body: T) async -> Data? where T: Encodable {
|
||||
let bodyData = try! API.encoder.encode(body)
|
||||
return await post(route, bodyData: bodyData)
|
||||
}
|
||||
|
||||
private func post(_ route: Route, bodyData: Data) async -> Data? {
|
||||
guard let url = url else {
|
||||
return nil
|
||||
}
|
||||
var request = URLRequest(url: url.appendingPathComponent(route.rawValue))
|
||||
request.httpBody = bodyData
|
||||
request.httpMethod = "POST"
|
||||
do {
|
||||
let (data, response) = try await URLSession.shared.data(for: request)
|
||||
guard let httpResponse = response as? HTTPURLResponse else {
|
||||
return nil
|
||||
}
|
||||
guard httpResponse.statusCode == 200 else {
|
||||
print("Failed with code: \(httpResponse.statusCode)")
|
||||
return nil
|
||||
}
|
||||
return data
|
||||
} catch {
|
||||
print("Failed with error: \(error)")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -1,21 +1,326 @@
|
||||
//
|
||||
// ContentView.swift
|
||||
// FlurSchnaps
|
||||
//
|
||||
// Created by CH on 04.06.22.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import APNSwift
|
||||
import PushAPI
|
||||
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 = API()
|
||||
|
||||
@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 {
|
||||
Text("Hello, world!")
|
||||
.padding()
|
||||
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: $api.server)
|
||||
.padding(.horizontal, 50)
|
||||
.padding(.top)
|
||||
TextEntryField("Application", placeholder: "register-device-application-placeholder", symbol: .questionmarkApp, showClearButton: true, text: $api.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 {
|
||||
DeviceList(pushToken: push,
|
||||
authToken: auth,
|
||||
api: api,
|
||||
isPresented: $showDeviceList,
|
||||
devices: deviceList)
|
||||
}
|
||||
}.onAppear {
|
||||
startPeriodicUpdates()
|
||||
}.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
|
||||
}
|
||||
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 else {
|
||||
return
|
||||
}
|
||||
Task {
|
||||
let confirmed = await api.isConfirmed(token: token, authentication: authToken)
|
||||
if !confirmed {
|
||||
print("Not confirmed by server: \(api.url?.path ?? "No server") (\(api.server))")
|
||||
print(token.base64EncodedString())
|
||||
print(authToken.base64EncodedString())
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
isConfirmed = confirmed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func updateDeviceList() {
|
||||
guard let authToken = authToken,
|
||||
let pushToken = pushToken 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 else {
|
||||
return
|
||||
}
|
||||
guard let pushToken = pushToken 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")
|
||||
}
|
||||
}
|
||||
|
42
FlurSchnaps/Data+Extensions.swift
Normal file
42
FlurSchnaps/Data+Extensions.swift
Normal file
@ -0,0 +1,42 @@
|
||||
import Foundation
|
||||
|
||||
extension Data {
|
||||
|
||||
public var hexEncoded: String {
|
||||
return map { String(format: "%02hhx", $0) }.joined()
|
||||
}
|
||||
|
||||
// Convert 0 ... 9, a ... f, A ...F to their decimal value,
|
||||
// return nil for all other input characters
|
||||
private func decodeNibble(_ u: UInt16) -> UInt8? {
|
||||
switch(u) {
|
||||
case 0x30 ... 0x39:
|
||||
return UInt8(u - 0x30)
|
||||
case 0x41 ... 0x46:
|
||||
return UInt8(u - 0x41 + 10)
|
||||
case 0x61 ... 0x66:
|
||||
return UInt8(u - 0x61 + 10)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
public init?(fromHexEncodedString string: String) {
|
||||
let utf16 = string.utf16
|
||||
self.init(capacity: utf16.count/2)
|
||||
|
||||
var i = utf16.startIndex
|
||||
guard utf16.count % 2 == 0 else {
|
||||
return nil
|
||||
}
|
||||
while i != utf16.endIndex {
|
||||
guard let hi = decodeNibble(utf16[i]),
|
||||
let lo = decodeNibble(utf16[utf16.index(i, offsetBy: 1, limitedBy: utf16.endIndex)!]) else {
|
||||
return nil
|
||||
}
|
||||
var value = hi << 4 + lo
|
||||
self.append(&value, count: 1)
|
||||
i = utf16.index(i, offsetBy: 2, limitedBy: utf16.endIndex)!
|
||||
}
|
||||
}
|
||||
}
|
90
FlurSchnaps/DeviceList.swift
Normal file
90
FlurSchnaps/DeviceList.swift
Normal file
@ -0,0 +1,90 @@
|
||||
import SwiftUI
|
||||
import PushAPI
|
||||
|
||||
extension String {
|
||||
|
||||
var nonEmpty: String? {
|
||||
isEmpty ? nil : self
|
||||
}
|
||||
}
|
||||
|
||||
struct DeviceList: View {
|
||||
|
||||
let pushToken: PushToken
|
||||
|
||||
let authToken: AuthenticationToken
|
||||
|
||||
let api: API
|
||||
|
||||
@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)")
|
||||
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")!),
|
||||
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
|
||||
}
|
||||
}
|
8
FlurSchnaps/FlurSchnaps.entitlements
Normal file
8
FlurSchnaps/FlurSchnaps.entitlements
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>aps-environment</key>
|
||||
<string>development</string>
|
||||
</dict>
|
||||
</plist>
|
@ -1,17 +1,72 @@
|
||||
//
|
||||
// FlurSchnapsApp.swift
|
||||
// FlurSchnaps
|
||||
//
|
||||
// Created by CH on 04.06.22.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
@main
|
||||
struct FlurSchnapsApp: App {
|
||||
|
||||
@UIApplicationDelegateAdaptor(AppDelegate.self)
|
||||
var appDelegate
|
||||
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
ContentView()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class AppDelegate: NSObject, UIApplicationDelegate {
|
||||
|
||||
@AppStorage("pushToken")
|
||||
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
|
||||
|
||||
|
||||
UIApplication.shared.registerForRemoteNotifications()
|
||||
return true
|
||||
}
|
||||
|
||||
func application(_ application: UIApplication,
|
||||
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
|
||||
print("Registered with token: \(deviceToken)")
|
||||
self.pushToken = deviceToken
|
||||
}
|
||||
|
||||
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any],
|
||||
fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
|
||||
|
||||
print(userInfo)
|
||||
|
||||
completionHandler(UIBackgroundFetchResult.newData)
|
||||
}
|
||||
}
|
||||
|
||||
extension AppDelegate: UNUserNotificationCenterDelegate {
|
||||
|
||||
// Receive displayed notifications for iOS 10 devices.
|
||||
func userNotificationCenter(_ center: UNUserNotificationCenter,
|
||||
willPresent notification: UNNotification,
|
||||
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
|
||||
let userInfo = notification.request.content.userInfo
|
||||
print(userInfo)
|
||||
|
||||
// Change this to your preferred presentation option
|
||||
completionHandler([[.banner, .badge, .sound]])
|
||||
}
|
||||
|
||||
|
||||
func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
|
||||
|
||||
}
|
||||
|
||||
func userNotificationCenter(_ center: UNUserNotificationCenter,
|
||||
didReceive response: UNNotificationResponse,
|
||||
withCompletionHandler completionHandler: @escaping () -> Void) {
|
||||
let userInfo = response.notification.request.content.userInfo
|
||||
print(userInfo)
|
||||
completionHandler()
|
||||
}
|
||||
}
|
||||
|
10
FlurSchnaps/Info.plist
Normal file
10
FlurSchnaps/Info.plist
Normal file
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>remote-notification</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
59
FlurSchnaps/TextEntryField.swift
Normal file
59
FlurSchnaps/TextEntryField.swift
Normal file
@ -0,0 +1,59 @@
|
||||
import SwiftUI
|
||||
import SFSafeSymbols
|
||||
|
||||
struct TextEntryField: View {
|
||||
|
||||
let name: String
|
||||
|
||||
let placeholder: LocalizedStringKey
|
||||
|
||||
let symbol: SFSymbol
|
||||
|
||||
let showClearButton: Bool
|
||||
|
||||
@Binding
|
||||
var text: String
|
||||
|
||||
init(_ name: String, placeholder: LocalizedStringKey, symbol: SFSymbol, showClearButton: Bool = false, text: Binding<String>) {
|
||||
self.name = name
|
||||
self.placeholder = placeholder
|
||||
self.symbol = symbol
|
||||
self.showClearButton = showClearButton
|
||||
self._text = text
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
TextField(name, text: $text, prompt: Text(placeholder))
|
||||
.padding(7)
|
||||
.padding(.horizontal, 25)
|
||||
.background(Color(.systemGray5))
|
||||
.cornerRadius(8)
|
||||
.overlay(
|
||||
HStack {
|
||||
Image(systemSymbol: symbol)
|
||||
.foregroundColor(.gray)
|
||||
.frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
|
||||
.padding(.leading, 8)
|
||||
if showClearButton && text != "" {
|
||||
Button(action: {
|
||||
self.text = ""
|
||||
}) {
|
||||
Image(systemSymbol: .multiplyCircleFill)
|
||||
.foregroundColor(.gray)
|
||||
.padding(.trailing, 8)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
struct TextEntryField_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
TextEntryField("Test",
|
||||
placeholder: "Enter text...",
|
||||
symbol: .textformat,
|
||||
text: .constant(""))
|
||||
.previewLayout(.fixed(width: 375, height: 50))
|
||||
}
|
||||
}
|
41
FlurSchnaps/de.lproj/Localizable.strings
Normal file
41
FlurSchnaps/de.lproj/Localizable.strings
Normal file
@ -0,0 +1,41 @@
|
||||
"remote-notifications-title" = "Remote notifications";
|
||||
|
||||
"notification-permissions-title" = "Benachrichtigungen";
|
||||
|
||||
"request-notification-permission-button" = "Erlaubnis erteilen";
|
||||
|
||||
"push-server-registration-title" = "Geräte-Registrierung";
|
||||
|
||||
"other-devices-title" = "Verfügbare Geräte";
|
||||
|
||||
"register-for-remote-notifications-text" = "Das Gerät konnte sich nicht für Benachrichtigungen anmelden. Bitte überprüfe deine Internetverbindung.";
|
||||
|
||||
"register-for-remote-notifications-button" = "Erneut versuchen";
|
||||
|
||||
"no-notification-permissions-text" = "Die App hat keine Berechtigung zum Anzeigen von Benachrichtigungen. Bitte ändere die Einstellungen.";
|
||||
|
||||
"no-notification-permissions-button" = "Einstellungen";
|
||||
|
||||
"register-device-text" = "Registriere das Gerät mit einem Push Server, um Benachrichtigungen zu erhalten.";
|
||||
|
||||
"register-device-server-placeholder" = "Server";
|
||||
|
||||
"register-device-application-placeholder" = "Anwendung";
|
||||
|
||||
"register-device-name-placeholder" = "Name des Geräts";
|
||||
|
||||
"register-device-button" = "Gerät registrieren";
|
||||
|
||||
"push-message-description" = "Sende eine Benachrichtigung an alle Geräte, mit einem optionalen Text.";
|
||||
|
||||
"push-message-title-placeholder" = "Titel";
|
||||
|
||||
"push-message-placeholder" = "Nachricht";
|
||||
|
||||
"toggle-include-own-device-text" = "Eigenes Gerät benachrichtigen";
|
||||
|
||||
"send-notification-button" = "Nachricht senden";
|
||||
|
||||
"device-list-title" = "Geräte";
|
||||
|
||||
"show-device-list-button" = "Liste einzeigen";
|
41
FlurSchnaps/en.lproj/Localizable.strings
Normal file
41
FlurSchnaps/en.lproj/Localizable.strings
Normal file
@ -0,0 +1,41 @@
|
||||
"remote-notifications-title" = "Remote notifications";
|
||||
|
||||
"notification-permissions-title" = "Notification permissions";
|
||||
|
||||
"request-notification-permission-button" = "Enable notifications";
|
||||
|
||||
"push-server-registration-title" = "Push server registration";
|
||||
|
||||
"other-devices-title" = "Other devices available";
|
||||
|
||||
"register-for-remote-notifications-text" = "The device could not register for remote notifications. Check your internet connection.";
|
||||
|
||||
"register-for-remote-notifications-button" = "Retry";
|
||||
|
||||
"no-notification-permissions-text" = "The app doesn't have permission to display notifications. Please change the permissions for the app to work.";
|
||||
|
||||
"no-notification-permissions-button" = "Device settings";
|
||||
|
||||
"register-device-text" = "Register the device with the push server to receive notifications.";
|
||||
|
||||
"register-device-server-placeholder" = "Server url";
|
||||
|
||||
"register-device-application-placeholder" = "Application ID";
|
||||
|
||||
"register-device-name-placeholder" = "Device name";
|
||||
|
||||
"register-device-button" = "Register device";
|
||||
|
||||
"push-message-description" = "Send a push notification to all other devices, with an optional custom text";
|
||||
|
||||
"push-message-title-placeholder" = "Message title";
|
||||
|
||||
"push-message-placeholder" = "Message text";
|
||||
|
||||
"toggle-include-own-device-text" = "Include own device";
|
||||
|
||||
"send-notification-button" = "Send notification";
|
||||
|
||||
"device-list-title" = "Devices";
|
||||
|
||||
"show-device-list-button" = "Show device list";
|
Reference in New Issue
Block a user