Initial commit
This commit is contained in:
381
apple-client/PrivateChatApple.xcodeproj/project.pbxproj
Normal file
381
apple-client/PrivateChatApple.xcodeproj/project.pbxproj
Normal file
@@ -0,0 +1,381 @@
|
||||
// !$*UTF8*$!
|
||||
{
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 77;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
0CF99BBF9D1A04B9E2F5DFC8 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A07E2A8E4DEFCE88D5553B5B /* ContentView.swift */; };
|
||||
1A645AC02B000D539C39368C /* PasskeyManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38FF93F2A1FD25DB9E957AA6 /* PasskeyManager.swift */; };
|
||||
3B90CADE868971A576AE57A7 /* AppModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 16E71E386D6B12B015DC0103 /* AppModels.swift */; };
|
||||
3BEB977AA98055EF05F97989 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 977F5A04FC063DC80BB9CE26 /* SettingsView.swift */; };
|
||||
861A055BD65866A57B6BBC0E /* EmbeddedWebAppView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09649C45C63E1AD1704C0D78 /* EmbeddedWebAppView.swift */; };
|
||||
D02DE21893F9BFC1519D511C /* BackendClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 591916AA72EF25C5E6F3CCBB /* BackendClient.swift */; };
|
||||
E854B030B8ACED01056B39FD /* PrivateChatAppleApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF8901583248E9FADEEB357 /* PrivateChatAppleApp.swift */; };
|
||||
FD0B1329E00BFC563CA92B34 /* SettingsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8826CE950551697F8A2238FD /* SettingsStore.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
08E8FBDE6277D997AAD09FFE /* PrivateChatApple.app */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.application; path = PrivateChatApple.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
09649C45C63E1AD1704C0D78 /* EmbeddedWebAppView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmbeddedWebAppView.swift; sourceTree = "<group>"; };
|
||||
16E71E386D6B12B015DC0103 /* AppModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppModels.swift; sourceTree = "<group>"; };
|
||||
38FF93F2A1FD25DB9E957AA6 /* PasskeyManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasskeyManager.swift; sourceTree = "<group>"; };
|
||||
591916AA72EF25C5E6F3CCBB /* BackendClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackendClient.swift; sourceTree = "<group>"; };
|
||||
8826CE950551697F8A2238FD /* SettingsStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsStore.swift; sourceTree = "<group>"; };
|
||||
977F5A04FC063DC80BB9CE26 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
|
||||
A07E2A8E4DEFCE88D5553B5B /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
|
||||
FDF8901583248E9FADEEB357 /* PrivateChatAppleApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivateChatAppleApp.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
63815A93152EA7A84CECAE92 /* App */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
16E71E386D6B12B015DC0103 /* AppModels.swift */,
|
||||
591916AA72EF25C5E6F3CCBB /* BackendClient.swift */,
|
||||
A07E2A8E4DEFCE88D5553B5B /* ContentView.swift */,
|
||||
09649C45C63E1AD1704C0D78 /* EmbeddedWebAppView.swift */,
|
||||
38FF93F2A1FD25DB9E957AA6 /* PasskeyManager.swift */,
|
||||
FDF8901583248E9FADEEB357 /* PrivateChatAppleApp.swift */,
|
||||
8826CE950551697F8A2238FD /* SettingsStore.swift */,
|
||||
977F5A04FC063DC80BB9CE26 /* SettingsView.swift */,
|
||||
);
|
||||
name = App;
|
||||
path = Sources/App;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
66E649CB0A00080FE1C2DC25 /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
08E8FBDE6277D997AAD09FFE /* PrivateChatApple.app */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
CBDB6BA6F0BD33470718D8EF = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
63815A93152EA7A84CECAE92 /* App */,
|
||||
66E649CB0A00080FE1C2DC25 /* Products */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
3438777E81C60FEE7970CAF2 /* PrivateChatApple */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 03EBB318C449F01FB0639745 /* Build configuration list for PBXNativeTarget "PrivateChatApple" */;
|
||||
buildPhases = (
|
||||
473BDD4E02B992BD309F5028 /* Build Embedded Angular Client */,
|
||||
0B1A447B1617CF6E1E4F4EA9 /* Sources */,
|
||||
04166E69061744AF9B27795E /* Copy Embedded Angular Client */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = PrivateChatApple;
|
||||
packageProductDependencies = (
|
||||
);
|
||||
productName = PrivateChatApple;
|
||||
productReference = 08E8FBDE6277D997AAD09FFE /* PrivateChatApple.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
12B40BE574717A46063F45C8 /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
BuildIndependentTargetsInParallel = YES;
|
||||
LastUpgradeCheck = 1430;
|
||||
TargetAttributes = {
|
||||
3438777E81C60FEE7970CAF2 = {
|
||||
ProvisioningStyle = Automatic;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = A55C1DFA666ADF74C56D34DA /* Build configuration list for PBXProject "PrivateChatApple" */;
|
||||
compatibilityVersion = "Xcode 14.0";
|
||||
developmentRegion = en;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
Base,
|
||||
en,
|
||||
);
|
||||
mainGroup = CBDB6BA6F0BD33470718D8EF;
|
||||
minimizedProjectReferenceProxies = 1;
|
||||
preferredProjectObjectVersion = 77;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
3438777E81C60FEE7970CAF2 /* PrivateChatApple */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
||||
/* Begin PBXShellScriptBuildPhase section */
|
||||
04166E69061744AF9B27795E /* Copy Embedded Angular Client */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "Copy Embedded Angular Client";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "set -euo pipefail\nDESTINATION=\"$TARGET_BUILD_DIR/$UNLOCALIZED_RESOURCES_FOLDER_PATH/WebApp\"\nrm -rf \"$DESTINATION\"\nmkdir -p \"$DESTINATION\"\ncp -R \"$SRCROOT/WebApp/.\" \"$DESTINATION\"\n";
|
||||
};
|
||||
473BDD4E02B992BD309F5028 /* Build Embedded Angular Client */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "Build Embedded Angular Client";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "set -euo pipefail\ncd \"$SRCROOT/..\"\nmkdir -p \"$SRCROOT/WebApp\"\nnpm run build --prefix client -- --base-href ./ --output-path \"$SRCROOT/WebApp\"\n";
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
0B1A447B1617CF6E1E4F4EA9 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
3B90CADE868971A576AE57A7 /* AppModels.swift in Sources */,
|
||||
D02DE21893F9BFC1519D511C /* BackendClient.swift in Sources */,
|
||||
0CF99BBF9D1A04B9E2F5DFC8 /* ContentView.swift in Sources */,
|
||||
861A055BD65866A57B6BBC0E /* EmbeddedWebAppView.swift in Sources */,
|
||||
1A645AC02B000D539C39368C /* PasskeyManager.swift in Sources */,
|
||||
E854B030B8ACED01056B39FD /* PrivateChatAppleApp.swift in Sources */,
|
||||
FD0B1329E00BFC563CA92B34 /* SettingsStore.swift in Sources */,
|
||||
3BEB977AA98055EF05F97989 /* SettingsView.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
0356A63804EF725F92924B9B /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGNING_ALLOWED = NO;
|
||||
CODE_SIGNING_REQUIRED = NO;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
INFOPLIST_KEY_CFBundleDisplayName = PrivateChat;
|
||||
INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO;
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_NAME = PrivateChatApple;
|
||||
SDKROOT = auto;
|
||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx";
|
||||
SUPPORTS_MACCATALYST = NO;
|
||||
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = YES;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
2E89E0E6371B30AE59C3646B /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 17.4;
|
||||
MACOSX_DEPLOYMENT_TARGET = 14.4;
|
||||
MARKETING_VERSION = 1.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.privatechat.apple;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = auto;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||
SWIFT_VERSION = 6.0;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
8269FBF8D3B1C19B7CA65298 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GCC_DYNAMIC_NO_PIC = NO;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_OPTIMIZATION_LEVEL = 0;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"$(inherited)",
|
||||
"DEBUG=1",
|
||||
);
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 17.4;
|
||||
MACOSX_DEPLOYMENT_TARGET = 14.4;
|
||||
MARKETING_VERSION = 1.0;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.privatechat.apple;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = auto;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 6.0;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
9A5E4F1577926AC0869DBE31 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGNING_ALLOWED = NO;
|
||||
CODE_SIGNING_REQUIRED = NO;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
INFOPLIST_KEY_CFBundleDisplayName = PrivateChat;
|
||||
INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO;
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_NAME = PrivateChatApple;
|
||||
SDKROOT = auto;
|
||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx";
|
||||
SUPPORTS_MACCATALYST = NO;
|
||||
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = YES;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
03EBB318C449F01FB0639745 /* Build configuration list for PBXNativeTarget "PrivateChatApple" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
9A5E4F1577926AC0869DBE31 /* Debug */,
|
||||
0356A63804EF725F92924B9B /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Debug;
|
||||
};
|
||||
A55C1DFA666ADF74C56D34DA /* Build configuration list for PBXProject "PrivateChatApple" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
8269FBF8D3B1C19B7CA65298 /* Debug */,
|
||||
2E89E0E6371B30AE59C3646B /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Debug;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
};
|
||||
rootObject = 12B40BE574717A46063F45C8 /* Project object */;
|
||||
}
|
||||
7
apple-client/PrivateChatApple.xcodeproj/project.xcworkspace/contents.xcworkspacedata
generated
Normal file
7
apple-client/PrivateChatApple.xcodeproj/project.xcworkspace/contents.xcworkspacedata
generated
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "self:">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
Binary file not shown.
@@ -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>PrivateChatApple.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>0</integer>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
133
apple-client/Sources/App/AppModels.swift
Normal file
133
apple-client/Sources/App/AppModels.swift
Normal file
@@ -0,0 +1,133 @@
|
||||
import Foundation
|
||||
|
||||
struct UserProfile: Codable, Equatable {
|
||||
let id: String
|
||||
let username: String
|
||||
let displayName: String
|
||||
}
|
||||
|
||||
struct AuthResponse: Codable {
|
||||
let token: String
|
||||
let user: UserProfile
|
||||
}
|
||||
|
||||
struct AccessKeySummary: Codable, Equatable, Identifiable {
|
||||
let id: String
|
||||
let credentialId: String
|
||||
let label: String
|
||||
let transports: [String]
|
||||
let deviceType: String
|
||||
let backedUp: Bool
|
||||
let aaguid: String
|
||||
let createdAt: String
|
||||
}
|
||||
|
||||
struct AccessKeyListResponse: Codable {
|
||||
let credentials: [AccessKeySummary]
|
||||
}
|
||||
|
||||
struct SessionResponse: Codable {
|
||||
let user: UserProfile
|
||||
}
|
||||
|
||||
struct RegistrationOptionsResponse: Codable {
|
||||
struct RelyingParty: Codable {
|
||||
let name: String
|
||||
let id: String
|
||||
}
|
||||
|
||||
struct UserEntity: Codable {
|
||||
let id: String
|
||||
let name: String
|
||||
let displayName: String
|
||||
}
|
||||
|
||||
let expectedOrigin: String?
|
||||
let rp: RelyingParty
|
||||
let user: UserEntity
|
||||
let challenge: String
|
||||
}
|
||||
|
||||
struct AuthenticationOptionsResponse: Codable {
|
||||
let attemptId: String
|
||||
let expectedOrigin: String?
|
||||
let challenge: String
|
||||
let rpId: String?
|
||||
}
|
||||
|
||||
struct APIErrorResponse: Codable, Error {
|
||||
let message: String
|
||||
}
|
||||
|
||||
struct PasskeyRegistrationPayload: Encodable {
|
||||
struct Response: Encodable {
|
||||
let clientDataJSON: String
|
||||
let attestationObject: String
|
||||
let transports: [String]?
|
||||
}
|
||||
|
||||
let id: String
|
||||
let rawId: String
|
||||
let response: Response
|
||||
let clientExtensionResults: [String: String]
|
||||
let type: String
|
||||
}
|
||||
|
||||
struct PasskeyAuthenticationPayload: Encodable {
|
||||
struct Response: Encodable {
|
||||
let clientDataJSON: String
|
||||
let authenticatorData: String
|
||||
let signature: String
|
||||
let userHandle: String?
|
||||
}
|
||||
|
||||
let id: String
|
||||
let rawId: String
|
||||
let response: Response
|
||||
let clientExtensionResults: [String: String]
|
||||
let type: String
|
||||
}
|
||||
|
||||
extension Data {
|
||||
init?(base64URLEncoded value: String) {
|
||||
var normalized = value.replacingOccurrences(of: "-", with: "+").replacingOccurrences(of: "_", with: "/")
|
||||
let padding = (4 - normalized.count % 4) % 4
|
||||
normalized.append(String(repeating: "=", count: padding))
|
||||
self.init(base64Encoded: normalized)
|
||||
}
|
||||
|
||||
func base64URLEncodedString() -> String {
|
||||
base64EncodedString()
|
||||
.replacingOccurrences(of: "+", with: "-")
|
||||
.replacingOccurrences(of: "/", with: "_")
|
||||
.replacingOccurrences(of: "=", with: "")
|
||||
}
|
||||
}
|
||||
|
||||
extension Encodable {
|
||||
func jsonString() -> String {
|
||||
let encoder = JSONEncoder()
|
||||
encoder.outputFormatting = [.withoutEscapingSlashes]
|
||||
|
||||
guard let data = try? encoder.encode(AnyEncodable(self)),
|
||||
let string = String(data: data, encoding: .utf8)
|
||||
else {
|
||||
return "null"
|
||||
}
|
||||
|
||||
return string
|
||||
}
|
||||
}
|
||||
|
||||
private struct AnyEncodable: Encodable {
|
||||
private let encodeValue: (Encoder) throws -> Void
|
||||
|
||||
init(_ value: some Encodable) {
|
||||
encodeValue = value.encode
|
||||
}
|
||||
|
||||
func encode(to encoder: Encoder) throws {
|
||||
try encodeValue(encoder)
|
||||
}
|
||||
}
|
||||
|
||||
150
apple-client/Sources/App/BackendClient.swift
Normal file
150
apple-client/Sources/App/BackendClient.swift
Normal file
@@ -0,0 +1,150 @@
|
||||
import Foundation
|
||||
|
||||
struct BackendClient {
|
||||
let baseURL: URL
|
||||
|
||||
func register(username: String, password: String, displayName: String) async throws -> AuthResponse {
|
||||
try await post(
|
||||
path: "/api/auth/register",
|
||||
body: RegisterRequest(
|
||||
username: username,
|
||||
password: password,
|
||||
displayName: displayName.isEmpty ? nil : displayName
|
||||
),
|
||||
token: nil
|
||||
)
|
||||
}
|
||||
|
||||
func login(username: String, password: String) async throws -> AuthResponse {
|
||||
try await post(
|
||||
path: "/api/auth/login",
|
||||
body: LoginRequest(username: username, password: password),
|
||||
token: nil
|
||||
)
|
||||
}
|
||||
|
||||
func restoreSession(token: String) async throws -> SessionResponse {
|
||||
try await get(path: "/api/auth/session", token: token)
|
||||
}
|
||||
|
||||
func logout(token: String) async throws {
|
||||
let _: EmptyResponse = try await post(path: "/api/auth/logout", body: EmptyBody(), token: token)
|
||||
}
|
||||
|
||||
func listAccessKeys(token: String) async throws -> [AccessKeySummary] {
|
||||
let response: AccessKeyListResponse = try await get(path: "/api/webauthn/credentials", token: token)
|
||||
return response.credentials
|
||||
}
|
||||
|
||||
func startAccessKeyRegistration(label: String?, token: String) async throws -> RegistrationOptionsResponse {
|
||||
try await post(
|
||||
path: "/api/webauthn/register/options",
|
||||
body: RegisterAccessKeyRequest(label: label?.isEmpty == false ? label : nil),
|
||||
token: token
|
||||
)
|
||||
}
|
||||
|
||||
func finishAccessKeyRegistration(payload: PasskeyRegistrationPayload, token: String) async throws {
|
||||
let _: EmptyResponse = try await post(
|
||||
path: "/api/webauthn/register/verify",
|
||||
body: VerifyRegistrationRequest(credential: payload),
|
||||
token: token
|
||||
)
|
||||
}
|
||||
|
||||
func startAccessKeyAuthentication() async throws -> AuthenticationOptionsResponse {
|
||||
try await post(path: "/api/webauthn/authenticate/options", body: EmptyBody(), token: nil)
|
||||
}
|
||||
|
||||
func finishAccessKeyAuthentication(
|
||||
attemptId: String,
|
||||
payload: PasskeyAuthenticationPayload
|
||||
) async throws -> AuthResponse {
|
||||
try await post(
|
||||
path: "/api/webauthn/authenticate/verify",
|
||||
body: VerifyAuthenticationRequest(attemptId: attemptId, credential: payload),
|
||||
token: nil
|
||||
)
|
||||
}
|
||||
|
||||
private func get<Response: Decodable>(path: String, token: String?) async throws -> Response {
|
||||
var request = URLRequest(url: baseURL.appending(path: path))
|
||||
request.httpMethod = "GET"
|
||||
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
||||
|
||||
if let token {
|
||||
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
|
||||
}
|
||||
|
||||
return try await perform(request)
|
||||
}
|
||||
|
||||
private func post<Body: Encodable, Response: Decodable>(
|
||||
path: String,
|
||||
body: Body,
|
||||
token: String?
|
||||
) async throws -> Response {
|
||||
var request = URLRequest(url: baseURL.appending(path: path))
|
||||
request.httpMethod = "POST"
|
||||
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
||||
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
|
||||
if let token {
|
||||
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
|
||||
}
|
||||
|
||||
request.httpBody = try JSONEncoder().encode(body)
|
||||
return try await perform(request)
|
||||
}
|
||||
|
||||
private func perform<Response: Decodable>(_ request: URLRequest) async throws -> Response {
|
||||
let (data, rawResponse) = try await URLSession.shared.data(for: request)
|
||||
let response = rawResponse as? HTTPURLResponse
|
||||
|
||||
guard let response else {
|
||||
throw APIErrorResponse(message: "The backend did not return a valid HTTP response.")
|
||||
}
|
||||
|
||||
let decoder = JSONDecoder()
|
||||
|
||||
if (200 ..< 300).contains(response.statusCode) {
|
||||
if Response.self == EmptyResponse.self {
|
||||
return EmptyResponse() as! Response
|
||||
}
|
||||
|
||||
return try decoder.decode(Response.self, from: data)
|
||||
}
|
||||
|
||||
if let apiError = try? decoder.decode(APIErrorResponse.self, from: data) {
|
||||
throw apiError
|
||||
}
|
||||
|
||||
throw APIErrorResponse(message: "The backend returned HTTP \(response.statusCode).")
|
||||
}
|
||||
}
|
||||
|
||||
private struct EmptyBody: Encodable {}
|
||||
private struct EmptyResponse: Decodable {}
|
||||
private struct RegisterRequest: Encodable {
|
||||
let username: String
|
||||
let password: String
|
||||
let displayName: String?
|
||||
}
|
||||
|
||||
private struct LoginRequest: Encodable {
|
||||
let username: String
|
||||
let password: String
|
||||
}
|
||||
|
||||
private struct RegisterAccessKeyRequest: Encodable {
|
||||
let label: String?
|
||||
}
|
||||
|
||||
private struct VerifyRegistrationRequest: Encodable {
|
||||
let credential: PasskeyRegistrationPayload
|
||||
}
|
||||
|
||||
private struct VerifyAuthenticationRequest: Encodable {
|
||||
let attemptId: String
|
||||
let credential: PasskeyAuthenticationPayload
|
||||
}
|
||||
57
apple-client/Sources/App/ContentView.swift
Normal file
57
apple-client/Sources/App/ContentView.swift
Normal file
@@ -0,0 +1,57 @@
|
||||
import SwiftUI
|
||||
|
||||
struct ContentView: View {
|
||||
@Bindable var settings: SettingsStore
|
||||
|
||||
#if !os(macOS)
|
||||
@State private var showSettings = false
|
||||
#endif
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
EmbeddedWebAppView(settings: settings)
|
||||
.toolbar {
|
||||
#if os(macOS)
|
||||
ToolbarItem(placement: .primaryAction) {
|
||||
SettingsLink {
|
||||
Image(systemName: "gearshape")
|
||||
}
|
||||
}
|
||||
#else
|
||||
ToolbarItem(placement: .topBarTrailing) {
|
||||
Button {
|
||||
showSettings = true
|
||||
} label: {
|
||||
Image(systemName: "gearshape")
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
.overlay(alignment: .bottom) {
|
||||
if let user = settings.currentUser {
|
||||
Text("Signed in as \(user.displayName)")
|
||||
.font(.footnote)
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.vertical, 8)
|
||||
.background(.ultraThinMaterial, in: Capsule())
|
||||
.padding(.bottom, 16)
|
||||
}
|
||||
}
|
||||
#if !os(macOS)
|
||||
.sheet(isPresented: $showSettings) {
|
||||
NavigationStack {
|
||||
SettingsView(settings: settings)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .topBarTrailing) {
|
||||
Button("Done") {
|
||||
showSettings = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
112
apple-client/Sources/App/EmbeddedWebAppView.swift
Normal file
112
apple-client/Sources/App/EmbeddedWebAppView.swift
Normal file
@@ -0,0 +1,112 @@
|
||||
import SwiftUI
|
||||
import WebKit
|
||||
|
||||
struct EmbeddedWebAppView: View {
|
||||
@Bindable var settings: SettingsStore
|
||||
|
||||
var body: some View {
|
||||
PlatformWebView(script: settings.injectionScript, reloadToken: settings.webStateVersion)
|
||||
.ignoresSafeArea()
|
||||
}
|
||||
}
|
||||
|
||||
#if os(macOS)
|
||||
struct PlatformWebView: NSViewRepresentable {
|
||||
let script: String
|
||||
let reloadToken: UUID
|
||||
|
||||
func makeCoordinator() -> Coordinator {
|
||||
Coordinator()
|
||||
}
|
||||
|
||||
func makeNSView(context: Context) -> WKWebView {
|
||||
context.coordinator.makeWebView(script: script)
|
||||
}
|
||||
|
||||
func updateNSView(_ webView: WKWebView, context: Context) {
|
||||
context.coordinator.update(webView: webView, script: script, reloadToken: reloadToken)
|
||||
}
|
||||
}
|
||||
#else
|
||||
struct PlatformWebView: UIViewRepresentable {
|
||||
let script: String
|
||||
let reloadToken: UUID
|
||||
|
||||
func makeCoordinator() -> Coordinator {
|
||||
Coordinator()
|
||||
}
|
||||
|
||||
func makeUIView(context: Context) -> WKWebView {
|
||||
context.coordinator.makeWebView(script: script)
|
||||
}
|
||||
|
||||
func updateUIView(_ webView: WKWebView, context: Context) {
|
||||
context.coordinator.update(webView: webView, script: script, reloadToken: reloadToken)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
final class Coordinator: NSObject, WKNavigationDelegate {
|
||||
private var lastReloadToken: UUID?
|
||||
private var pendingRefreshTask: Task<Void, Never>?
|
||||
|
||||
func makeWebView(script: String) -> WKWebView {
|
||||
let configuration = WKWebViewConfiguration()
|
||||
let contentController = WKUserContentController()
|
||||
contentController.addUserScript(
|
||||
WKUserScript(source: script, injectionTime: .atDocumentStart, forMainFrameOnly: true)
|
||||
)
|
||||
configuration.userContentController = contentController
|
||||
|
||||
let webView = WKWebView(frame: .zero, configuration: configuration)
|
||||
webView.navigationDelegate = self
|
||||
#if os(macOS)
|
||||
webView.setValue(false, forKey: "drawsBackground")
|
||||
#endif
|
||||
loadIndex(into: webView)
|
||||
return webView
|
||||
}
|
||||
|
||||
func update(webView: WKWebView, script: String, reloadToken: UUID) {
|
||||
guard lastReloadToken != reloadToken else {
|
||||
return
|
||||
}
|
||||
|
||||
lastReloadToken = reloadToken
|
||||
pendingRefreshTask?.cancel()
|
||||
pendingRefreshTask = Task { @MainActor [weak webView] in
|
||||
await Task.yield()
|
||||
|
||||
guard !Task.isCancelled, let webView else {
|
||||
return
|
||||
}
|
||||
|
||||
webView.evaluateJavaScript(script) { _, _ in
|
||||
DispatchQueue.main.async {
|
||||
webView.reload()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func loadIndex(into webView: WKWebView) {
|
||||
let indexURL =
|
||||
Bundle.main.url(forResource: "index", withExtension: "html", subdirectory: "WebApp/browser")
|
||||
?? Bundle.main.url(forResource: "index", withExtension: "html", subdirectory: "WebApp")
|
||||
|
||||
guard let indexURL else {
|
||||
let fallbackHTML = """
|
||||
<html>
|
||||
<body style="font-family:-apple-system;padding:32px;background:#08111d;color:#eff3ff">
|
||||
<h2>Embedded chat bundle not found</h2>
|
||||
<p>The Apple app expects the Angular client to be built into the bundled <code>WebApp</code> folder.</p>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
webView.loadHTMLString(fallbackHTML, baseURL: nil)
|
||||
return
|
||||
}
|
||||
|
||||
webView.loadFileURL(indexURL, allowingReadAccessTo: indexURL.deletingLastPathComponent())
|
||||
}
|
||||
}
|
||||
126
apple-client/Sources/App/PasskeyManager.swift
Normal file
126
apple-client/Sources/App/PasskeyManager.swift
Normal file
@@ -0,0 +1,126 @@
|
||||
import AuthenticationServices
|
||||
import Foundation
|
||||
|
||||
#if os(macOS)
|
||||
import AppKit
|
||||
#else
|
||||
import UIKit
|
||||
#endif
|
||||
|
||||
@MainActor
|
||||
final class PasskeyManager: NSObject {
|
||||
private var continuation: CheckedContinuation<ASAuthorization, Error>?
|
||||
|
||||
func register(options: RegistrationOptionsResponse) async throws -> PasskeyRegistrationPayload {
|
||||
guard let challengeData = Data(base64URLEncoded: options.challenge),
|
||||
let userData = Data(base64URLEncoded: options.user.id)
|
||||
else {
|
||||
throw APIErrorResponse(message: "The backend returned malformed access key registration options.")
|
||||
}
|
||||
|
||||
let provider = ASAuthorizationPlatformPublicKeyCredentialProvider(relyingPartyIdentifier: options.rp.id)
|
||||
let clientData = ASPublicKeyCredentialClientData(
|
||||
challenge: challengeData,
|
||||
origin: options.expectedOrigin ?? "http://localhost:4200"
|
||||
)
|
||||
let request = provider.createCredentialRegistrationRequest(
|
||||
clientData: clientData,
|
||||
name: options.user.name,
|
||||
userID: userData
|
||||
)
|
||||
|
||||
let authorization = try await perform(requests: [request])
|
||||
|
||||
guard let registration = authorization.credential as? ASAuthorizationPlatformPublicKeyCredentialRegistration else {
|
||||
throw APIErrorResponse(message: "The platform did not return a valid access key registration.")
|
||||
}
|
||||
|
||||
return PasskeyRegistrationPayload(
|
||||
id: registration.credentialID.base64URLEncodedString(),
|
||||
rawId: registration.credentialID.base64URLEncodedString(),
|
||||
response: .init(
|
||||
clientDataJSON: registration.rawClientDataJSON.base64URLEncodedString(),
|
||||
attestationObject: registration.rawAttestationObject?.base64URLEncodedString() ?? "",
|
||||
transports: nil
|
||||
),
|
||||
clientExtensionResults: [:],
|
||||
type: "public-key"
|
||||
)
|
||||
}
|
||||
|
||||
func authenticate(options: AuthenticationOptionsResponse) async throws -> PasskeyAuthenticationPayload {
|
||||
guard let challengeData = Data(base64URLEncoded: options.challenge) else {
|
||||
throw APIErrorResponse(message: "The backend returned malformed access key sign-in options.")
|
||||
}
|
||||
|
||||
let rpId = options.rpId
|
||||
?? URL(string: options.expectedOrigin ?? "")?.host
|
||||
?? "localhost"
|
||||
let provider = ASAuthorizationPlatformPublicKeyCredentialProvider(relyingPartyIdentifier: rpId)
|
||||
let clientData = ASPublicKeyCredentialClientData(
|
||||
challenge: challengeData,
|
||||
origin: options.expectedOrigin ?? "http://localhost:4200"
|
||||
)
|
||||
let request = provider.createCredentialAssertionRequest(clientData: clientData)
|
||||
let authorization = try await perform(requests: [request])
|
||||
|
||||
guard let assertion = authorization.credential as? ASAuthorizationPlatformPublicKeyCredentialAssertion else {
|
||||
throw APIErrorResponse(message: "The platform did not return a valid access key assertion.")
|
||||
}
|
||||
|
||||
return PasskeyAuthenticationPayload(
|
||||
id: assertion.credentialID.base64URLEncodedString(),
|
||||
rawId: assertion.credentialID.base64URLEncodedString(),
|
||||
response: .init(
|
||||
clientDataJSON: assertion.rawClientDataJSON.base64URLEncodedString(),
|
||||
authenticatorData: assertion.rawAuthenticatorData.base64URLEncodedString(),
|
||||
signature: assertion.signature.base64URLEncodedString(),
|
||||
userHandle: assertion.userID.base64URLEncodedString()
|
||||
),
|
||||
clientExtensionResults: [:],
|
||||
type: "public-key"
|
||||
)
|
||||
}
|
||||
|
||||
private func perform(requests: [ASAuthorizationRequest]) async throws -> ASAuthorization {
|
||||
try await withCheckedThrowingContinuation { continuation in
|
||||
self.continuation = continuation
|
||||
let controller = ASAuthorizationController(authorizationRequests: requests)
|
||||
controller.delegate = self
|
||||
controller.presentationContextProvider = self
|
||||
controller.performRequests()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension PasskeyManager: ASAuthorizationControllerDelegate {
|
||||
func authorizationController(
|
||||
controller _: ASAuthorizationController,
|
||||
didCompleteWithAuthorization authorization: ASAuthorization
|
||||
) {
|
||||
continuation?.resume(returning: authorization)
|
||||
continuation = nil
|
||||
}
|
||||
|
||||
func authorizationController(
|
||||
controller _: ASAuthorizationController,
|
||||
didCompleteWithError error: Error
|
||||
) {
|
||||
continuation?.resume(throwing: error)
|
||||
continuation = nil
|
||||
}
|
||||
}
|
||||
|
||||
extension PasskeyManager: ASAuthorizationControllerPresentationContextProviding {
|
||||
func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor {
|
||||
#if os(macOS)
|
||||
return NSApplication.shared.keyWindow ?? NSApplication.shared.windows.first ?? ASPresentationAnchor()
|
||||
#else
|
||||
return UIApplication.shared.connectedScenes
|
||||
.compactMap { $0 as? UIWindowScene }
|
||||
.flatMap(\.windows)
|
||||
.first(where: \.isKeyWindow) ?? ASPresentationAnchor()
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
18
apple-client/Sources/App/PrivateChatAppleApp.swift
Normal file
18
apple-client/Sources/App/PrivateChatAppleApp.swift
Normal file
@@ -0,0 +1,18 @@
|
||||
import SwiftUI
|
||||
|
||||
@main
|
||||
struct PrivateChatAppleApp: App {
|
||||
@State private var settings = SettingsStore()
|
||||
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
ContentView(settings: settings)
|
||||
}
|
||||
|
||||
#if os(macOS)
|
||||
Settings {
|
||||
SettingsView(settings: settings)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
227
apple-client/Sources/App/SettingsStore.swift
Normal file
227
apple-client/Sources/App/SettingsStore.swift
Normal file
@@ -0,0 +1,227 @@
|
||||
import Foundation
|
||||
import Observation
|
||||
|
||||
@MainActor
|
||||
@Observable
|
||||
final class SettingsStore {
|
||||
enum AuthMode: String, CaseIterable, Identifiable {
|
||||
case login
|
||||
case register
|
||||
|
||||
var id: String { rawValue }
|
||||
}
|
||||
|
||||
var backendURLString: String
|
||||
var currentUser: UserProfile?
|
||||
var accessKeys: [AccessKeySummary]
|
||||
var authMode: AuthMode = .login
|
||||
var username = ""
|
||||
var password = ""
|
||||
var displayName = ""
|
||||
var accessKeyLabel = ""
|
||||
var infoMessage: String?
|
||||
var errorMessage: String?
|
||||
var isBusy = false
|
||||
private(set) var webStateVersion = UUID()
|
||||
|
||||
private let defaults = UserDefaults.standard
|
||||
private let passkeyManager = PasskeyManager()
|
||||
|
||||
private enum Keys {
|
||||
static let backendURL = "privatechat.apple.backendURL"
|
||||
static let token = "privatechat.apple.token"
|
||||
static let user = "privatechat.apple.user"
|
||||
}
|
||||
|
||||
init() {
|
||||
backendURLString = defaults.string(forKey: Keys.backendURL) ?? "http://localhost:3000"
|
||||
accessKeys = []
|
||||
|
||||
if let data = defaults.data(forKey: Keys.user),
|
||||
let user = try? JSONDecoder().decode(UserProfile.self, from: data)
|
||||
{
|
||||
currentUser = user
|
||||
}
|
||||
|
||||
Task {
|
||||
await restoreSessionIfPossible()
|
||||
}
|
||||
}
|
||||
|
||||
var token: String? {
|
||||
defaults.string(forKey: Keys.token)
|
||||
}
|
||||
|
||||
var isAuthenticated: Bool {
|
||||
token != nil && currentUser != nil
|
||||
}
|
||||
|
||||
var injectionScript: String {
|
||||
let serverValue = backendURLString.jsonString()
|
||||
let tokenValue = (token ?? "").jsonString()
|
||||
let userValue: String
|
||||
|
||||
if let currentUser {
|
||||
userValue = currentUser.jsonString()
|
||||
} else {
|
||||
userValue = "null"
|
||||
}
|
||||
|
||||
return """
|
||||
(function() {
|
||||
try {
|
||||
localStorage.setItem('privatechat.embeddedMode', '1');
|
||||
localStorage.setItem('privatechat.serverUrl', \(serverValue));
|
||||
if (\(tokenValue) && \(tokenValue) !== '""') {
|
||||
localStorage.setItem('privatechat.token', \(tokenValue));
|
||||
} else {
|
||||
localStorage.removeItem('privatechat.token');
|
||||
}
|
||||
if (\(userValue) !== null) {
|
||||
localStorage.setItem('privatechat.user', JSON.stringify(\(userValue)));
|
||||
} else {
|
||||
localStorage.removeItem('privatechat.user');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to sync native settings into localStorage', error);
|
||||
}
|
||||
})();
|
||||
"""
|
||||
}
|
||||
|
||||
func saveBackendURL() {
|
||||
defaults.set(backendURLString, forKey: Keys.backendURL)
|
||||
invalidateWebState(info: "Backend URL updated.")
|
||||
}
|
||||
|
||||
func authenticate() async {
|
||||
await runTask { [self] in
|
||||
switch authMode {
|
||||
case .login:
|
||||
let response = try await self.apiClient().login(username: self.username, password: self.password)
|
||||
try await self.applyAuthResponse(response, success: "Signed in as \(response.user.displayName).")
|
||||
case .register:
|
||||
let response = try await self.apiClient().register(
|
||||
username: self.username,
|
||||
password: self.password,
|
||||
displayName: self.displayName
|
||||
)
|
||||
try await self.applyAuthResponse(response, success: "Account created for \(response.user.displayName).")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func signInWithAccessKey() async {
|
||||
await runTask { [self] in
|
||||
let options = try await self.apiClient().startAccessKeyAuthentication()
|
||||
let credential = try await self.passkeyManager.authenticate(options: options)
|
||||
let response = try await self.apiClient().finishAccessKeyAuthentication(
|
||||
attemptId: options.attemptId,
|
||||
payload: credential
|
||||
)
|
||||
try await self.applyAuthResponse(response, success: "Signed in as \(response.user.displayName).")
|
||||
}
|
||||
}
|
||||
|
||||
func registerAccessKey() async {
|
||||
guard let token else {
|
||||
errorMessage = "Sign in before registering an access key."
|
||||
return
|
||||
}
|
||||
|
||||
await runTask { [self] in
|
||||
let options = try await self.apiClient().startAccessKeyRegistration(
|
||||
label: self.accessKeyLabel.isEmpty ? nil : self.accessKeyLabel,
|
||||
token: token
|
||||
)
|
||||
let payload = try await self.passkeyManager.register(options: options)
|
||||
try await self.apiClient().finishAccessKeyRegistration(payload: payload, token: token)
|
||||
self.accessKeyLabel = ""
|
||||
self.accessKeys = try await self.apiClient().listAccessKeys(token: token)
|
||||
self.infoMessage = "Access key registered."
|
||||
}
|
||||
}
|
||||
|
||||
func signOut() async {
|
||||
guard let token else {
|
||||
clearAuthState(info: "Signed out.")
|
||||
return
|
||||
}
|
||||
|
||||
await runTask { [self] in
|
||||
try? await self.apiClient().logout(token: token)
|
||||
self.clearAuthState(info: "Signed out.")
|
||||
self.authMode = .login
|
||||
}
|
||||
}
|
||||
|
||||
func restoreSessionIfPossible() async {
|
||||
guard let token else {
|
||||
return
|
||||
}
|
||||
|
||||
await runTask(clearMessages: false) { [self] in
|
||||
let response = try await self.apiClient().restoreSession(token: token)
|
||||
self.currentUser = response.user
|
||||
self.accessKeys = try await self.apiClient().listAccessKeys(token: token)
|
||||
self.infoMessage = "Restored session for \(response.user.displayName)."
|
||||
self.invalidateWebState()
|
||||
}
|
||||
}
|
||||
|
||||
private func apiClient() throws -> BackendClient {
|
||||
guard let url = URL(string: backendURLString.trimmingCharacters(in: .whitespacesAndNewlines)),
|
||||
url.scheme?.hasPrefix("http") == true
|
||||
else {
|
||||
throw APIErrorResponse(message: "Enter a valid backend URL before continuing.")
|
||||
}
|
||||
|
||||
return BackendClient(baseURL: url)
|
||||
}
|
||||
|
||||
private func applyAuthResponse(_ response: AuthResponse, success: String) async throws {
|
||||
currentUser = response.user
|
||||
defaults.set(response.token, forKey: Keys.token)
|
||||
defaults.set(try JSONEncoder().encode(response.user), forKey: Keys.user)
|
||||
accessKeys = try await apiClient().listAccessKeys(token: response.token)
|
||||
password = ""
|
||||
infoMessage = success
|
||||
invalidateWebState()
|
||||
}
|
||||
|
||||
private func clearAuthState(info: String) {
|
||||
currentUser = nil
|
||||
accessKeys = []
|
||||
defaults.removeObject(forKey: Keys.token)
|
||||
defaults.removeObject(forKey: Keys.user)
|
||||
infoMessage = info
|
||||
errorMessage = nil
|
||||
invalidateWebState()
|
||||
}
|
||||
|
||||
private func invalidateWebState(info: String? = nil) {
|
||||
if let info {
|
||||
infoMessage = info
|
||||
}
|
||||
|
||||
webStateVersion = UUID()
|
||||
}
|
||||
|
||||
private func runTask(clearMessages: Bool = true, operation: @escaping () async throws -> Void) async {
|
||||
if clearMessages {
|
||||
errorMessage = nil
|
||||
infoMessage = nil
|
||||
}
|
||||
|
||||
isBusy = true
|
||||
defer { isBusy = false }
|
||||
|
||||
do {
|
||||
try await operation()
|
||||
} catch let apiError as APIErrorResponse {
|
||||
errorMessage = apiError.message
|
||||
} catch {
|
||||
errorMessage = error.localizedDescription
|
||||
}
|
||||
}
|
||||
}
|
||||
127
apple-client/Sources/App/SettingsView.swift
Normal file
127
apple-client/Sources/App/SettingsView.swift
Normal file
@@ -0,0 +1,127 @@
|
||||
import SwiftUI
|
||||
|
||||
struct SettingsView: View {
|
||||
@Bindable var settings: SettingsStore
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
Section("Backend") {
|
||||
TextField("Backend URL", text: $settings.backendURLString)
|
||||
.onSubmit {
|
||||
settings.saveBackendURL()
|
||||
}
|
||||
.platformCredentialField()
|
||||
|
||||
Button("Apply Backend URL") {
|
||||
settings.saveBackendURL()
|
||||
}
|
||||
}
|
||||
|
||||
Section("Session") {
|
||||
if let user = settings.currentUser {
|
||||
LabeledContent("Signed in as", value: user.displayName)
|
||||
LabeledContent("Username", value: user.username)
|
||||
|
||||
Button("Sign out", role: .destructive) {
|
||||
Task {
|
||||
await settings.signOut()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Picker("Mode", selection: $settings.authMode) {
|
||||
ForEach(SettingsStore.AuthMode.allCases) { mode in
|
||||
Text(mode == .login ? "Log in" : "Register").tag(mode)
|
||||
}
|
||||
}
|
||||
.pickerStyle(.segmented)
|
||||
|
||||
if settings.authMode == .register {
|
||||
TextField("Display name", text: $settings.displayName)
|
||||
}
|
||||
|
||||
TextField("Username", text: $settings.username)
|
||||
.platformCredentialField()
|
||||
|
||||
SecureField("Password", text: $settings.password)
|
||||
|
||||
Button(settings.authMode == .login ? "Authenticate" : "Create account") {
|
||||
Task {
|
||||
await settings.authenticate()
|
||||
}
|
||||
}
|
||||
.disabled(settings.isBusy)
|
||||
|
||||
Button("Use access key") {
|
||||
Task {
|
||||
await settings.signInWithAccessKey()
|
||||
}
|
||||
}
|
||||
.disabled(settings.isBusy)
|
||||
}
|
||||
}
|
||||
|
||||
Section("Access Keys") {
|
||||
if settings.currentUser == nil {
|
||||
Text("Sign in before registering or listing access keys.")
|
||||
.foregroundStyle(.secondary)
|
||||
} else {
|
||||
TextField("New access key label", text: $settings.accessKeyLabel)
|
||||
Button("Register access key") {
|
||||
Task {
|
||||
await settings.registerAccessKey()
|
||||
}
|
||||
}
|
||||
.disabled(settings.isBusy)
|
||||
|
||||
if settings.accessKeys.isEmpty {
|
||||
Text("No access keys registered yet.")
|
||||
.foregroundStyle(.secondary)
|
||||
} else {
|
||||
ForEach(settings.accessKeys) { key in
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(key.label)
|
||||
.font(.headline)
|
||||
Text("Device: \(key.deviceType)\(key.backedUp ? " / backed up" : "")")
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.secondary)
|
||||
Text("Transports: \(key.transports.isEmpty ? "unspecified" : key.transports.joined(separator: ", "))")
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
.padding(.vertical, 4)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let infoMessage = settings.infoMessage {
|
||||
Section("Status") {
|
||||
Text(infoMessage)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
if let errorMessage = settings.errorMessage {
|
||||
Section("Error") {
|
||||
Text(errorMessage)
|
||||
.foregroundStyle(.red)
|
||||
}
|
||||
}
|
||||
}
|
||||
.formStyle(.grouped)
|
||||
.padding()
|
||||
.frame(minWidth: 420, minHeight: 520)
|
||||
}
|
||||
}
|
||||
|
||||
private extension View {
|
||||
@ViewBuilder
|
||||
func platformCredentialField() -> some View {
|
||||
#if os(iOS)
|
||||
textInputAutocapitalization(.never)
|
||||
.autocorrectionDisabled()
|
||||
#else
|
||||
self
|
||||
#endif
|
||||
}
|
||||
}
|
||||
355
apple-client/WebApp/3rdpartylicenses.txt
Normal file
355
apple-client/WebApp/3rdpartylicenses.txt
Normal file
@@ -0,0 +1,355 @@
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
Package: @angular/core
|
||||
License: "MIT"
|
||||
|
||||
The MIT License
|
||||
|
||||
Copyright (c) 2010-2026 Google LLC. https://angular.dev/license
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
Package: rxjs
|
||||
License: "Apache-2.0"
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright (c) 2015-2018 Google, Inc., Netflix, Inc., Microsoft Corp. and contributors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
Package: tslib
|
||||
License: "0BSD"
|
||||
|
||||
Copyright (c) Microsoft Corporation.
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
PERFORMANCE OF THIS SOFTWARE.
|
||||
--------------------------------------------------------------------------------
|
||||
Package: @angular/common
|
||||
License: "MIT"
|
||||
|
||||
The MIT License
|
||||
|
||||
Copyright (c) 2010-2026 Google LLC. https://angular.dev/license
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
Package: @angular/platform-browser
|
||||
License: "MIT"
|
||||
|
||||
The MIT License
|
||||
|
||||
Copyright (c) 2010-2026 Google LLC. https://angular.dev/license
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
Package: @angular/router
|
||||
License: "MIT"
|
||||
|
||||
The MIT License
|
||||
|
||||
Copyright (c) 2010-2026 Google LLC. https://angular.dev/license
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
Package: @angular/forms
|
||||
License: "MIT"
|
||||
|
||||
The MIT License
|
||||
|
||||
Copyright (c) 2010-2026 Google LLC. https://angular.dev/license
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
BIN
apple-client/WebApp/browser/favicon.ico
Normal file
BIN
apple-client/WebApp/browser/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
16
apple-client/WebApp/browser/index.html
Normal file
16
apple-client/WebApp/browser/index.html
Normal file
File diff suppressed because one or more lines are too long
8
apple-client/WebApp/browser/main-YTU56RI2.js
Normal file
8
apple-client/WebApp/browser/main-YTU56RI2.js
Normal file
File diff suppressed because one or more lines are too long
1
apple-client/WebApp/browser/styles-YLPXNZVT.css
Normal file
1
apple-client/WebApp/browser/styles-YLPXNZVT.css
Normal file
File diff suppressed because one or more lines are too long
3
apple-client/WebApp/prerendered-routes.json
Normal file
3
apple-client/WebApp/prerendered-routes.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"routes": {}
|
||||
}
|
||||
44
apple-client/project.yml
Normal file
44
apple-client/project.yml
Normal file
@@ -0,0 +1,44 @@
|
||||
name: PrivateChatApple
|
||||
options:
|
||||
bundleIdPrefix: com.privatechat
|
||||
settings:
|
||||
base:
|
||||
SWIFT_VERSION: 6.0
|
||||
PRODUCT_BUNDLE_IDENTIFIER: com.privatechat.apple
|
||||
MARKETING_VERSION: 1.0
|
||||
CURRENT_PROJECT_VERSION: 1
|
||||
GENERATE_INFOPLIST_FILE: YES
|
||||
IPHONEOS_DEPLOYMENT_TARGET: 17.4
|
||||
MACOSX_DEPLOYMENT_TARGET: 14.4
|
||||
CODE_SIGN_STYLE: Automatic
|
||||
targets:
|
||||
PrivateChatApple:
|
||||
type: application
|
||||
supportedDestinations:
|
||||
- iOS
|
||||
- macOS
|
||||
sources:
|
||||
- path: Sources/App
|
||||
settings:
|
||||
base:
|
||||
PRODUCT_NAME: PrivateChatApple
|
||||
INFOPLIST_KEY_CFBundleDisplayName: PrivateChat
|
||||
INFOPLIST_KEY_LSApplicationCategoryType: public.app-category.social-networking
|
||||
INFOPLIST_KEY_ITSAppUsesNonExemptEncryption: NO
|
||||
CODE_SIGNING_ALLOWED: NO
|
||||
CODE_SIGNING_REQUIRED: NO
|
||||
preBuildScripts:
|
||||
- name: Build Embedded Angular Client
|
||||
script: |
|
||||
set -euo pipefail
|
||||
cd "$SRCROOT/.."
|
||||
mkdir -p "$SRCROOT/WebApp"
|
||||
npm run build --prefix client -- --base-href ./ --output-path "$SRCROOT/WebApp"
|
||||
postBuildScripts:
|
||||
- name: Copy Embedded Angular Client
|
||||
script: |
|
||||
set -euo pipefail
|
||||
DESTINATION="$TARGET_BUILD_DIR/$UNLOCALIZED_RESOURCES_FOLDER_PATH/WebApp"
|
||||
rm -rf "$DESTINATION"
|
||||
mkdir -p "$DESTINATION"
|
||||
cp -R "$SRCROOT/WebApp/." "$DESTINATION"
|
||||
Reference in New Issue
Block a user