First working version
This commit is contained in:
parent
5ffea2c2c1
commit
25fbaef134
@ -7,18 +7,33 @@
|
||||
objects = {
|
||||
|
||||
/* 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 */; };
|
||||
E2349959284E0695002B55F8 /* PushAPI in Frameworks */ = {isa = PBXBuildFile; productRef = E2349958284E0695002B55F8 /* PushAPI */; };
|
||||
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 */; };
|
||||
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 */; };
|
||||
E29A7E4E284B6144000B908A /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E29A7E4D284B6144000B908A /* Preview Assets.xcassets */; };
|
||||
E29A7E55284B619A000B908A /* API.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29A7E54284B619A000B908A /* API.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
881E0B25284B74E200435EC2 /* Data+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+Extensions.swift"; sourceTree = "<group>"; };
|
||||
88DBE727284B7EB200D1573B /* FlurSchnaps.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = FlurSchnaps.entitlements; sourceTree = "<group>"; };
|
||||
88DBE728284B813500D1573B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
|
||||
88DBE729284B989C00D1573B /* DeviceList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceList.swift; sourceTree = "<group>"; };
|
||||
E2349960284E372B002B55F8 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
E2349962284E3733002B55F8 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
E2349963284F3133002B55F8 /* TextEntryField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextEntryField.swift; sourceTree = "<group>"; };
|
||||
E29A7E43284B6143000B908A /* FlurSchnaps.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = FlurSchnaps.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
E29A7E46284B6143000B908A /* FlurSchnapsApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlurSchnapsApp.swift; sourceTree = "<group>"; };
|
||||
E29A7E48284B6143000B908A /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
|
||||
E29A7E4A284B6144000B908A /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
E29A7E4D284B6144000B908A /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
|
||||
E29A7E54284B619A000B908A /* API.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = API.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
@ -26,6 +41,8 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
E234995C284E1D02002B55F8 /* SFSafeSymbols in Frameworks */,
|
||||
E2349959284E0695002B55F8 /* PushAPI in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@ -51,8 +68,15 @@
|
||||
E29A7E45284B6143000B908A /* FlurSchnaps */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
E2349961284E372B002B55F8 /* Localizable.strings */,
|
||||
88DBE728284B813500D1573B /* Info.plist */,
|
||||
88DBE727284B7EB200D1573B /* FlurSchnaps.entitlements */,
|
||||
E29A7E46284B6143000B908A /* FlurSchnapsApp.swift */,
|
||||
E29A7E48284B6143000B908A /* ContentView.swift */,
|
||||
E2349963284F3133002B55F8 /* TextEntryField.swift */,
|
||||
88DBE729284B989C00D1573B /* DeviceList.swift */,
|
||||
E29A7E54284B619A000B908A /* API.swift */,
|
||||
881E0B25284B74E200435EC2 /* Data+Extensions.swift */,
|
||||
E29A7E4A284B6144000B908A /* Assets.xcassets */,
|
||||
E29A7E4C284B6144000B908A /* Preview Content */,
|
||||
);
|
||||
@ -83,6 +107,10 @@
|
||||
dependencies = (
|
||||
);
|
||||
name = FlurSchnaps;
|
||||
packageProductDependencies = (
|
||||
E2349958284E0695002B55F8 /* PushAPI */,
|
||||
E234995B284E1D02002B55F8 /* SFSafeSymbols */,
|
||||
);
|
||||
productName = FlurSchnaps;
|
||||
productReference = E29A7E43284B6143000B908A /* FlurSchnaps.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
@ -109,8 +137,13 @@
|
||||
knownRegions = (
|
||||
en,
|
||||
Base,
|
||||
de,
|
||||
);
|
||||
mainGroup = E29A7E3A284B6143000B908A;
|
||||
packageReferences = (
|
||||
E2349957284E0695002B55F8 /* XCRemoteSwiftPackageReference "Push-API" */,
|
||||
E234995A284E1D02002B55F8 /* XCRemoteSwiftPackageReference "SFSafeSymbols" */,
|
||||
);
|
||||
productRefGroup = E29A7E44284B6143000B908A /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
@ -126,6 +159,7 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
E29A7E4E284B6144000B908A /* Preview Assets.xcassets in Resources */,
|
||||
E234995F284E372B002B55F8 /* Localizable.strings in Resources */,
|
||||
E29A7E4B284B6144000B908A /* Assets.xcassets in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@ -137,13 +171,29 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
E2349964284F3133002B55F8 /* TextEntryField.swift in Sources */,
|
||||
E29A7E49284B6143000B908A /* ContentView.swift in Sources */,
|
||||
881E0B26284B74E200435EC2 /* Data+Extensions.swift in Sources */,
|
||||
E29A7E55284B619A000B908A /* API.swift in Sources */,
|
||||
88DBE72A284B989C00D1573B /* DeviceList.swift in Sources */,
|
||||
E29A7E47284B6143000B908A /* FlurSchnapsApp.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXVariantGroup section */
|
||||
E2349961284E372B002B55F8 /* Localizable.strings */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
E2349960284E372B002B55F8 /* en */,
|
||||
E2349962284E3733002B55F8 /* de */,
|
||||
);
|
||||
name = Localizable.strings;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXVariantGroup section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
E29A7E4F284B6144000B908A /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
@ -264,17 +314,21 @@
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_ENTITLEMENTS = FlurSchnaps/FlurSchnaps.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_ASSET_PATHS = "\"FlurSchnaps/Preview Content\"";
|
||||
DEVELOPMENT_TEAM = H8WR4M6QQ4;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = FlurSchnaps/Info.plist;
|
||||
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = UIInterfaceOrientationPortrait;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.4;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
@ -284,7 +338,7 @@
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
TARGETED_DEVICE_FAMILY = 1;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
@ -293,17 +347,21 @@
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_ENTITLEMENTS = FlurSchnaps/FlurSchnaps.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_ASSET_PATHS = "\"FlurSchnaps/Preview Content\"";
|
||||
DEVELOPMENT_TEAM = H8WR4M6QQ4;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = FlurSchnaps/Info.plist;
|
||||
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = UIInterfaceOrientationPortrait;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.4;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
@ -313,7 +371,7 @@
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
TARGETED_DEVICE_FAMILY = 1;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
@ -339,6 +397,38 @@
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
|
||||
/* Begin XCRemoteSwiftPackageReference section */
|
||||
E2349957284E0695002B55F8 /* XCRemoteSwiftPackageReference "Push-API" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://christophhagen.de/git/ch/Push-API";
|
||||
requirement = {
|
||||
branch = main;
|
||||
kind = branch;
|
||||
};
|
||||
};
|
||||
E234995A284E1D02002B55F8 /* XCRemoteSwiftPackageReference "SFSafeSymbols" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/SFSafeSymbols/SFSafeSymbols";
|
||||
requirement = {
|
||||
kind = upToNextMajorVersion;
|
||||
minimumVersion = 3.0.0;
|
||||
};
|
||||
};
|
||||
/* End XCRemoteSwiftPackageReference section */
|
||||
|
||||
/* Begin XCSwiftPackageProductDependency section */
|
||||
E2349958284E0695002B55F8 /* PushAPI */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = E2349957284E0695002B55F8 /* XCRemoteSwiftPackageReference "Push-API" */;
|
||||
productName = PushAPI;
|
||||
};
|
||||
E234995B284E1D02002B55F8 /* SFSafeSymbols */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = E234995A284E1D02002B55F8 /* XCRemoteSwiftPackageReference "SFSafeSymbols" */;
|
||||
productName = SFSafeSymbols;
|
||||
};
|
||||
/* End XCSwiftPackageProductDependency section */
|
||||
};
|
||||
rootObject = E29A7E3B284B6143000B908A /* Project object */;
|
||||
}
|
||||
|
@ -0,0 +1,86 @@
|
||||
{
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "apnswift",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/swift-server-community/APNSwift.git",
|
||||
"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-api",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://christophhagen.de/git/ch/Push-API",
|
||||
"state" : {
|
||||
"branch" : "main",
|
||||
"revision" : "d5fb765ac998e2f731ec6fe8bf473a396af9b61c"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "sfsafesymbols",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/SFSafeSymbols/SFSafeSymbols",
|
||||
"state" : {
|
||||
"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
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
<?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>SchemeUserState</key>
|
||||
<dict>
|
||||
<key>FlurSchnaps.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>0</integer>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
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!")
|
||||
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";
|
Loading…
Reference in New Issue
Block a user