First working version
This commit is contained in:
parent
5ffea2c2c1
commit
25fbaef134
@ -7,18 +7,33 @@
|
|||||||
objects = {
|
objects = {
|
||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* 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 */; };
|
E29A7E47284B6143000B908A /* FlurSchnapsApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29A7E46284B6143000B908A /* FlurSchnapsApp.swift */; };
|
||||||
E29A7E49284B6143000B908A /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29A7E48284B6143000B908A /* ContentView.swift */; };
|
E29A7E49284B6143000B908A /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29A7E48284B6143000B908A /* ContentView.swift */; };
|
||||||
E29A7E4B284B6144000B908A /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E29A7E4A284B6144000B908A /* Assets.xcassets */; };
|
E29A7E4B284B6144000B908A /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E29A7E4A284B6144000B908A /* Assets.xcassets */; };
|
||||||
E29A7E4E284B6144000B908A /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E29A7E4D284B6144000B908A /* Preview 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 */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXFileReference 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; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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 */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
@ -26,6 +41,8 @@
|
|||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
E234995C284E1D02002B55F8 /* SFSafeSymbols in Frameworks */,
|
||||||
|
E2349959284E0695002B55F8 /* PushAPI in Frameworks */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@ -51,8 +68,15 @@
|
|||||||
E29A7E45284B6143000B908A /* FlurSchnaps */ = {
|
E29A7E45284B6143000B908A /* FlurSchnaps */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
E2349961284E372B002B55F8 /* Localizable.strings */,
|
||||||
|
88DBE728284B813500D1573B /* Info.plist */,
|
||||||
|
88DBE727284B7EB200D1573B /* FlurSchnaps.entitlements */,
|
||||||
E29A7E46284B6143000B908A /* FlurSchnapsApp.swift */,
|
E29A7E46284B6143000B908A /* FlurSchnapsApp.swift */,
|
||||||
E29A7E48284B6143000B908A /* ContentView.swift */,
|
E29A7E48284B6143000B908A /* ContentView.swift */,
|
||||||
|
E2349963284F3133002B55F8 /* TextEntryField.swift */,
|
||||||
|
88DBE729284B989C00D1573B /* DeviceList.swift */,
|
||||||
|
E29A7E54284B619A000B908A /* API.swift */,
|
||||||
|
881E0B25284B74E200435EC2 /* Data+Extensions.swift */,
|
||||||
E29A7E4A284B6144000B908A /* Assets.xcassets */,
|
E29A7E4A284B6144000B908A /* Assets.xcassets */,
|
||||||
E29A7E4C284B6144000B908A /* Preview Content */,
|
E29A7E4C284B6144000B908A /* Preview Content */,
|
||||||
);
|
);
|
||||||
@ -83,6 +107,10 @@
|
|||||||
dependencies = (
|
dependencies = (
|
||||||
);
|
);
|
||||||
name = FlurSchnaps;
|
name = FlurSchnaps;
|
||||||
|
packageProductDependencies = (
|
||||||
|
E2349958284E0695002B55F8 /* PushAPI */,
|
||||||
|
E234995B284E1D02002B55F8 /* SFSafeSymbols */,
|
||||||
|
);
|
||||||
productName = FlurSchnaps;
|
productName = FlurSchnaps;
|
||||||
productReference = E29A7E43284B6143000B908A /* FlurSchnaps.app */;
|
productReference = E29A7E43284B6143000B908A /* FlurSchnaps.app */;
|
||||||
productType = "com.apple.product-type.application";
|
productType = "com.apple.product-type.application";
|
||||||
@ -109,8 +137,13 @@
|
|||||||
knownRegions = (
|
knownRegions = (
|
||||||
en,
|
en,
|
||||||
Base,
|
Base,
|
||||||
|
de,
|
||||||
);
|
);
|
||||||
mainGroup = E29A7E3A284B6143000B908A;
|
mainGroup = E29A7E3A284B6143000B908A;
|
||||||
|
packageReferences = (
|
||||||
|
E2349957284E0695002B55F8 /* XCRemoteSwiftPackageReference "Push-API" */,
|
||||||
|
E234995A284E1D02002B55F8 /* XCRemoteSwiftPackageReference "SFSafeSymbols" */,
|
||||||
|
);
|
||||||
productRefGroup = E29A7E44284B6143000B908A /* Products */;
|
productRefGroup = E29A7E44284B6143000B908A /* Products */;
|
||||||
projectDirPath = "";
|
projectDirPath = "";
|
||||||
projectRoot = "";
|
projectRoot = "";
|
||||||
@ -126,6 +159,7 @@
|
|||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
E29A7E4E284B6144000B908A /* Preview Assets.xcassets in Resources */,
|
E29A7E4E284B6144000B908A /* Preview Assets.xcassets in Resources */,
|
||||||
|
E234995F284E372B002B55F8 /* Localizable.strings in Resources */,
|
||||||
E29A7E4B284B6144000B908A /* Assets.xcassets in Resources */,
|
E29A7E4B284B6144000B908A /* Assets.xcassets in Resources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
@ -137,13 +171,29 @@
|
|||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
E2349964284F3133002B55F8 /* TextEntryField.swift in Sources */,
|
||||||
E29A7E49284B6143000B908A /* ContentView.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 */,
|
E29A7E47284B6143000B908A /* FlurSchnapsApp.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
/* End PBXSourcesBuildPhase section */
|
/* 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 */
|
/* Begin XCBuildConfiguration section */
|
||||||
E29A7E4F284B6144000B908A /* Debug */ = {
|
E29A7E4F284B6144000B908A /* Debug */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
@ -264,17 +314,21 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
|
CODE_SIGN_ENTITLEMENTS = FlurSchnaps/FlurSchnaps.entitlements;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
DEVELOPMENT_ASSET_PATHS = "\"FlurSchnaps/Preview Content\"";
|
DEVELOPMENT_ASSET_PATHS = "\"FlurSchnaps/Preview Content\"";
|
||||||
DEVELOPMENT_TEAM = H8WR4M6QQ4;
|
DEVELOPMENT_TEAM = H8WR4M6QQ4;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
INFOPLIST_FILE = FlurSchnaps/Info.plist;
|
||||||
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
||||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
||||||
|
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
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 = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
@ -284,7 +338,7 @@
|
|||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TARGETED_DEVICE_FAMILY = "1,2";
|
TARGETED_DEVICE_FAMILY = 1;
|
||||||
};
|
};
|
||||||
name = Debug;
|
name = Debug;
|
||||||
};
|
};
|
||||||
@ -293,17 +347,21 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
|
CODE_SIGN_ENTITLEMENTS = FlurSchnaps/FlurSchnaps.entitlements;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
DEVELOPMENT_ASSET_PATHS = "\"FlurSchnaps/Preview Content\"";
|
DEVELOPMENT_ASSET_PATHS = "\"FlurSchnaps/Preview Content\"";
|
||||||
DEVELOPMENT_TEAM = H8WR4M6QQ4;
|
DEVELOPMENT_TEAM = H8WR4M6QQ4;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
INFOPLIST_FILE = FlurSchnaps/Info.plist;
|
||||||
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
||||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
||||||
|
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
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 = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
@ -313,7 +371,7 @@
|
|||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TARGETED_DEVICE_FAMILY = "1,2";
|
TARGETED_DEVICE_FAMILY = 1;
|
||||||
};
|
};
|
||||||
name = Release;
|
name = Release;
|
||||||
};
|
};
|
||||||
@ -339,6 +397,38 @@
|
|||||||
defaultConfigurationName = Release;
|
defaultConfigurationName = Release;
|
||||||
};
|
};
|
||||||
/* End XCConfigurationList section */
|
/* 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 */;
|
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 SwiftUI
|
||||||
|
import APNSwift
|
||||||
|
import PushAPI
|
||||||
|
import SFSafeSymbols
|
||||||
|
|
||||||
struct ContentView: View {
|
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 {
|
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()
|
.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 {
|
struct ContentView_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
ContentView()
|
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
|
import SwiftUI
|
||||||
|
|
||||||
@main
|
@main
|
||||||
struct FlurSchnapsApp: App {
|
struct FlurSchnapsApp: App {
|
||||||
|
|
||||||
|
@UIApplicationDelegateAdaptor(AppDelegate.self)
|
||||||
|
var appDelegate
|
||||||
|
|
||||||
var body: some Scene {
|
var body: some Scene {
|
||||||
WindowGroup {
|
WindowGroup {
|
||||||
ContentView()
|
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