diff --git a/api/Package.swift b/api/Package.swift index 0e8ad970..27f8e85e 100644 --- a/api/Package.swift +++ b/api/Package.swift @@ -18,6 +18,7 @@ let package = Package( .package(path: "../gertie"), .package(path: "../pairql"), .package(path: "../pairql-macapp"), + .package(path: "../pairql-iosapp"), .package(path: "../ts-interop"), .package(path: "../x-aws"), .package(path: "../x-sendgrid"), @@ -40,6 +41,7 @@ let package = Package( .product(name: "Gertie", package: "gertie"), .product(name: "PairQL", package: "pairql"), .product(name: "MacAppRoute", package: "pairql-macapp"), + .product(name: "IOSRoute", package: "pairql-iosapp"), .product(name: "TaggedTime", package: "swift-tagged"), .product(name: "VaporRouting", package: "vapor-routing"), .product(name: "Dependencies", package: "swift-dependencies"), diff --git a/api/Sources/Api/PairQL/MacApp/Resolvers/LogInterestingEvent.swift b/api/Sources/Api/PairQL/MacApp/Resolvers/LogInterestingEvent.swift index 86ae080f..ede49378 100644 --- a/api/Sources/Api/PairQL/MacApp/Resolvers/LogInterestingEvent.swift +++ b/api/Sources/Api/PairQL/MacApp/Resolvers/LogInterestingEvent.swift @@ -84,7 +84,7 @@ private func errorLoc(_ detail: String) -> String { } } -private func githubSearch(_ eventId: String) -> String { +func githubSearch(_ eventId: String) -> String { Slack.link( to: "https://github.com/search?q=repo%3Agertrude-app%2Fswift%20\(eventId)&type=code", withText: eventId diff --git a/api/Sources/Api/PairQL/iOS/LogIOSEvent.swift b/api/Sources/Api/PairQL/iOS/LogIOSEvent.swift new file mode 100644 index 00000000..10c1c26a --- /dev/null +++ b/api/Sources/Api/PairQL/iOS/LogIOSEvent.swift @@ -0,0 +1,25 @@ +import IOSRoute + +extension LogIOSEvent: Resolver { + static func resolve(with input: Input, in context: Context) async throws -> Output { + let detail = "\(input.detail ?? ""), " + [ + "device: `\(input.deviceType)`", + "iOS: `\(input.iOSVersion)`", + "vendorId: `\(input.vendorId?.lowercased ?? "(nil)")`", + ].joined(separator: ", ") + + try await context.db.create(InterestingEvent( + eventId: input.eventId, + kind: input.kind, + context: "ios", + detail: detail + )) + + if context.env.mode == .prod { + await with(dependency: \.slack) + .sysLog("iOS app event: \(githubSearch(input.eventId)) \(detail)") + } + + return .success + } +} diff --git a/api/Sources/Api/PairQL/iOS/iOSRoute.swift b/api/Sources/Api/PairQL/iOS/iOSRoute.swift new file mode 100644 index 00000000..ff696100 --- /dev/null +++ b/api/Sources/Api/PairQL/iOS/iOSRoute.swift @@ -0,0 +1,12 @@ +import IOSRoute +import Vapor + +extension IOSRoute: RouteResponder { + static func respond(to route: Self, in context: Context) async throws -> Response { + switch route { + case .logIOSEvent(let input): + let output = try await LogIOSEvent.resolve(with: input, in: context) + return try await self.respond(with: output) + } + } +} diff --git a/api/Sources/Api/Routes/PairQL.swift b/api/Sources/Api/Routes/PairQL.swift index c9699e6f..471b5293 100644 --- a/api/Sources/Api/Routes/PairQL.swift +++ b/api/Sources/Api/Routes/PairQL.swift @@ -1,4 +1,5 @@ import Dependencies +import IOSRoute import MacAppRoute import URLRouting import Vapor @@ -17,6 +18,7 @@ enum PairQLRoute: Equatable, RouteResponder { case dashboard(DashboardRoute) case macApp(MacAppRoute) case superAdmin(SuperAdminRoute) + case iOS(IOSRoute) nonisolated(unsafe) static let router = OneOf { Route(.case(PairQLRoute.macApp)) { @@ -24,6 +26,11 @@ enum PairQLRoute: Equatable, RouteResponder { Path { "macos-app" } MacAppRoute.router } + Route(.case(PairQLRoute.iOS)) { + Method("POST") + Path { "ios-app" } + IOSRoute.router + } Route(.case(PairQLRoute.dashboard)) { Method("POST") Path { "dashboard" } @@ -44,6 +51,8 @@ enum PairQLRoute: Equatable, RouteResponder { return try await DashboardRoute.respond(to: dashboardRoute, in: context) case .superAdmin(let superAdminRoute): return try await SuperAdminRoute.respond(to: superAdminRoute, in: context) + case .iOS(let iosAppRoute): + return try await IOSRoute.respond(to: iosAppRoute, in: context) } } @@ -107,6 +116,9 @@ private func logOperation(_ route: PairQLRoute, _ request: Request) { case .superAdmin: request.logger .notice("PairQL request: \("SuperAdmin".cyan) \(operation.yellow)") + case .iOS: + request.logger + .notice("PairQL request: \("iOS".blue) \(operation.yellow)") } } diff --git a/api/Tests/ApiTests/iOSPairResolvers/iOSResolverTests.swift b/api/Tests/ApiTests/iOSPairResolvers/iOSResolverTests.swift new file mode 100644 index 00000000..7651cfd4 --- /dev/null +++ b/api/Tests/ApiTests/iOSPairResolvers/iOSResolverTests.swift @@ -0,0 +1,35 @@ +import DuetSQL +import IOSRoute +import XCTest +import XExpect + +@testable import Api + +final class iOSResolverTests: ApiTestCase { + func testLogIOSEvent() async throws { + let eventId = UUID().uuidString + let vendorId = UUID() + _ = try await LogIOSEvent.resolve( + with: .init( + eventId: eventId, + kind: "event", + deviceType: "iPhone", + iOSVersion: "18.0.1", + vendorId: vendorId, + detail: "first launch" + ), + in: .mock + ) + + let retrieved = try await InterestingEvent.query() + .where(.eventId == eventId) + .first(in: self.db) + + expect(retrieved.kind).toEqual("event") + expect(retrieved.context).toEqual("ios") + expect(retrieved.detail!).toContain("iPhone") + expect(retrieved.detail!).toContain("18.0.1") + expect(retrieved.detail!).toContain(vendorId.lowercased) + expect(retrieved.detail!).toContain("first launch") + } +} diff --git a/iosapp/ios-poc.xcodeproj/project.pbxproj b/iosapp/Gertrude-iOS.xcodeproj/project.pbxproj similarity index 87% rename from iosapp/ios-poc.xcodeproj/project.pbxproj rename to iosapp/Gertrude-iOS.xcodeproj/project.pbxproj index 8818ff2d..b68b9e0e 100644 --- a/iosapp/ios-poc.xcodeproj/project.pbxproj +++ b/iosapp/Gertrude-iOS.xcodeproj/project.pbxproj @@ -10,8 +10,7 @@ 03E917BFA4D8CBC845611955 /* Colors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3943E5533FFA53791343F445 /* Colors.swift */; }; 1FE271D2FDBE4C6122B68A8A /* BgGradient.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7A47408B05AA4E9E2C4042A /* BgGradient.swift */; }; 580A5B546430D0B85CDE67A4 /* FeatureLI.swift in Sources */ = {isa = PBXBuildFile; fileRef = EFBB6E5EDFE1B2D7B40D17A6 /* FeatureLI.swift */; }; - 7F7E54B62CB576B20012844E /* Filter in Frameworks */ = {isa = PBXBuildFile; productRef = 7F7E54B52CB576B20012844E /* Filter */; }; - 7FA7B8472AEB238700363B53 /* ios_pocApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FA7B8462AEB238700363B53 /* ios_pocApp.swift */; }; + 7FA7B8472AEB238700363B53 /* IOSAppEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FA7B8462AEB238700363B53 /* IOSAppEntry.swift */; }; 7FA7B84B2AEB238800363B53 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 7FA7B84A2AEB238800363B53 /* Assets.xcassets */; }; 7FA7B84E2AEB238800363B53 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 7FA7B84D2AEB238800363B53 /* Preview Assets.xcassets */; }; 7FA7B85B2AEB243400363B53 /* NetworkExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7FA7B85A2AEB243400363B53 /* NetworkExtension.framework */; }; @@ -21,7 +20,8 @@ 7FE743D62C8F431500607876 /* FilterControlProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FE743D52C8F431500607876 /* FilterControlProvider.swift */; }; 7FE743DB2C8F431500607876 /* controller.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 7FE743D22C8F431500607876 /* controller.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 7FE743DF2C8F4ED700607876 /* NetworkExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7FA7B85A2AEB243400363B53 /* NetworkExtension.framework */; }; - 7FFB54912CB477C200EBFA3B /* App in Frameworks */ = {isa = PBXBuildFile; productRef = 7FFB54902CB477C200EBFA3B /* App */; }; + 7FEB782C2CC0058500A76A98 /* LibFilter in Frameworks */ = {isa = PBXBuildFile; productRef = 7FEB782B2CC0058500A76A98 /* LibFilter */; }; + 7FEC01B72CBF0DCA00B152B8 /* LibIOS in Frameworks */ = {isa = PBXBuildFile; productRef = 7FEC01B62CBF0DCA00B152B8 /* LibIOS */; }; CECCA8D914450391BB017E01 /* PrimaryButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FF145756F19546462ADE7D9 /* PrimaryButton.swift */; }; F7AEF6187A4EA032CA3A38F7 /* PreReqs.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7D8E4736A2DBCF7F38363B3 /* PreReqs.swift */; }; /* End PBXBuildFile section */ @@ -62,8 +62,8 @@ 3943E5533FFA53791343F445 /* Colors.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = Colors.swift; sourceTree = ""; }; 5FF145756F19546462ADE7D9 /* PrimaryButton.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PrimaryButton.swift; sourceTree = ""; }; 7F4A14502CB5843500FD6ABE /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; - 7FA7B8432AEB238700363B53 /* ios-poc.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "ios-poc.app"; sourceTree = BUILT_PRODUCTS_DIR; }; - 7FA7B8462AEB238700363B53 /* ios_pocApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ios_pocApp.swift; sourceTree = ""; }; + 7FA7B8432AEB238700363B53 /* app.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = app.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 7FA7B8462AEB238700363B53 /* IOSAppEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IOSAppEntry.swift; sourceTree = ""; }; 7FA7B84A2AEB238800363B53 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 7FA7B84D2AEB238800363B53 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 7FA7B8582AEB243400363B53 /* filter.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = filter.appex; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -71,7 +71,7 @@ 7FA7B85D2AEB243400363B53 /* FilterDataProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilterDataProvider.swift; sourceTree = ""; }; 7FA7B85F2AEB243400363B53 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 7FA7B8602AEB243400363B53 /* filter.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = filter.entitlements; sourceTree = ""; }; - 7FA7B8682AEB271800363B53 /* ios-poc.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "ios-poc.entitlements"; sourceTree = ""; }; + 7FA7B8682AEB271800363B53 /* app.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = app.entitlements; sourceTree = ""; }; 7FE743D22C8F431500607876 /* controller.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = controller.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 7FE743D52C8F431500607876 /* FilterControlProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilterControlProvider.swift; sourceTree = ""; }; 7FE743D72C8F431500607876 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -87,7 +87,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 7FFB54912CB477C200EBFA3B /* App in Frameworks */, + 7FEC01B72CBF0DCA00B152B8 /* LibIOS in Frameworks */, 7FE743DF2C8F4ED700607876 /* NetworkExtension.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -96,7 +96,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 7F7E54B62CB576B20012844E /* Filter in Frameworks */, + 7FEB782C2CC0058500A76A98 /* LibFilter in Frameworks */, 7FA7B85B2AEB243400363B53 /* NetworkExtension.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -120,19 +120,19 @@ path = Lib; sourceTree = ""; }; - 5C9A54EF758AD899FB94954C /* App */ = { + 5C9A54EF758AD899FB94954C /* LibIOS */ = { isa = PBXGroup; children = ( 9DAD2F4ECC8A0B77558A95F5 /* Views */, 545C6415C3C80BA9198F9BF5 /* Lib */, ); - path = App; + path = LibIOS; sourceTree = ""; }; 6F6AFDF4611779783100CDFA /* Sources */ = { isa = PBXGroup; children = ( - 5C9A54EF758AD899FB94954C /* App */, + 5C9A54EF758AD899FB94954C /* LibIOS */, ); path = Sources; sourceTree = ""; @@ -141,7 +141,7 @@ isa = PBXGroup; children = ( 7FFB548E2CB4768600EBFA3B /* lib-ios */, - 7FA7B8452AEB238700363B53 /* ios-poc */, + 7FA7B8452AEB238700363B53 /* app */, 7FA7B85C2AEB243400363B53 /* filter */, 7FE743D42C8F431500607876 /* controller */, 7FA7B8592AEB243400363B53 /* Frameworks */, @@ -152,23 +152,23 @@ 7FA7B8442AEB238700363B53 /* Products */ = { isa = PBXGroup; children = ( - 7FA7B8432AEB238700363B53 /* ios-poc.app */, + 7FA7B8432AEB238700363B53 /* app.app */, 7FA7B8582AEB243400363B53 /* filter.appex */, 7FE743D22C8F431500607876 /* controller.appex */, ); name = Products; sourceTree = ""; }; - 7FA7B8452AEB238700363B53 /* ios-poc */ = { + 7FA7B8452AEB238700363B53 /* app */ = { isa = PBXGroup; children = ( 7F4A14502CB5843500FD6ABE /* Info.plist */, - 7FA7B8682AEB271800363B53 /* ios-poc.entitlements */, - 7FA7B8462AEB238700363B53 /* ios_pocApp.swift */, + 7FA7B8682AEB271800363B53 /* app.entitlements */, + 7FA7B8462AEB238700363B53 /* IOSAppEntry.swift */, 7FA7B84A2AEB238800363B53 /* Assets.xcassets */, 7FA7B84C2AEB238800363B53 /* Preview Content */, ); - path = "ios-poc"; + path = app; sourceTree = ""; }; 7FA7B84C2AEB238800363B53 /* Preview Content */ = { @@ -230,9 +230,9 @@ /* End PBXGroup section */ /* Begin PBXNativeTarget section */ - 7FA7B8422AEB238700363B53 /* ios-poc */ = { + 7FA7B8422AEB238700363B53 /* app */ = { isa = PBXNativeTarget; - buildConfigurationList = 7FA7B8512AEB238800363B53 /* Build configuration list for PBXNativeTarget "ios-poc" */; + buildConfigurationList = 7FA7B8512AEB238800363B53 /* Build configuration list for PBXNativeTarget "app" */; buildPhases = ( 7FA7B83F2AEB238700363B53 /* Sources */, 7FA7B8402AEB238700363B53 /* Frameworks */, @@ -245,12 +245,12 @@ 7FA7B8622AEB243400363B53 /* PBXTargetDependency */, 7FE743DA2C8F431500607876 /* PBXTargetDependency */, ); - name = "ios-poc"; + name = app; packageProductDependencies = ( - 7FFB54902CB477C200EBFA3B /* App */, + 7FEC01B62CBF0DCA00B152B8 /* LibIOS */, ); - productName = "ios-poc"; - productReference = 7FA7B8432AEB238700363B53 /* ios-poc.app */; + productName = app; + productReference = 7FA7B8432AEB238700363B53 /* app.app */; productType = "com.apple.product-type.application"; }; 7FA7B8572AEB243400363B53 /* filter */ = { @@ -267,7 +267,7 @@ ); name = filter; packageProductDependencies = ( - 7F7E54B52CB576B20012844E /* Filter */, + 7FEB782B2CC0058500A76A98 /* LibFilter */, ); productName = filter; productReference = 7FA7B8582AEB243400363B53 /* filter.appex */; @@ -311,7 +311,7 @@ }; }; }; - buildConfigurationList = 7FA7B83E2AEB238700363B53 /* Build configuration list for PBXProject "ios-poc" */; + buildConfigurationList = 7FA7B83E2AEB238700363B53 /* Build configuration list for PBXProject "Gertrude-iOS" */; compatibilityVersion = "Xcode 14.0"; developmentRegion = en; hasScannedForEncodings = 0; @@ -327,7 +327,7 @@ projectDirPath = ""; projectRoot = ""; targets = ( - 7FA7B8422AEB238700363B53 /* ios-poc */, + 7FA7B8422AEB238700363B53 /* app */, 7FA7B8572AEB243400363B53 /* filter */, 7FE743D12C8F431500607876 /* controller */, ); @@ -365,12 +365,12 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 7FA7B8472AEB238700363B53 /* ios_pocApp.swift in Sources */, 1FE271D2FDBE4C6122B68A8A /* BgGradient.swift in Sources */, 580A5B546430D0B85CDE67A4 /* FeatureLI.swift in Sources */, 03E917BFA4D8CBC845611955 /* Colors.swift in Sources */, CECCA8D914450391BB017E01 /* PrimaryButton.swift in Sources */, F7AEF6187A4EA032CA3A38F7 /* PreReqs.swift in Sources */, + 7FA7B8472AEB238700363B53 /* IOSAppEntry.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -456,7 +456,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -510,7 +510,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = iphoneos; @@ -527,30 +527,31 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO; - CODE_SIGN_ENTITLEMENTS = "ios-poc/ios-poc.entitlements"; + CODE_SIGN_ENTITLEMENTS = app/app.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; - DEVELOPMENT_ASSET_PATHS = "\"ios-poc/Preview Content\""; - DEVELOPMENT_TEAM = ""; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"app/Preview Content\""; + DEVELOPMENT_TEAM = WFN83LM943; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = "ios-poc/Info.plist"; + INFOPLIST_FILE = app/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = Gertrude; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchStoryboardName = LaunchImage; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = UIInterfaceOrientationPortrait; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = "com.gertrude-skunk.ios-poc"; + MARKETING_VERSION = 1.0.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.netrivet.gertrude-ios.app"; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; @@ -565,30 +566,31 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO; - CODE_SIGN_ENTITLEMENTS = "ios-poc/ios-poc.entitlements"; + CODE_SIGN_ENTITLEMENTS = app/app.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; - DEVELOPMENT_ASSET_PATHS = "\"ios-poc/Preview Content\""; - DEVELOPMENT_TEAM = ""; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"app/Preview Content\""; + DEVELOPMENT_TEAM = WFN83LM943; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = "ios-poc/Info.plist"; + INFOPLIST_FILE = app/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = Gertrude; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchStoryboardName = LaunchImage; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = UIInterfaceOrientationPortrait; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = "com.gertrude-skunk.ios-poc"; + MARKETING_VERSION = 1.0.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.netrivet.gertrude-ios.app"; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; @@ -614,9 +616,12 @@ "@executable_path/../../Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = "com.gertrude-skunk.ios-poc.filter"; + PRODUCT_BUNDLE_IDENTIFIER = "com.netrivet.gertrude-ios.app.filter"; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; @@ -641,9 +646,12 @@ "@executable_path/../../Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = "com.gertrude-skunk.ios-poc.filter"; + PRODUCT_BUNDLE_IDENTIFIER = "com.netrivet.gertrude-ios.app.filter"; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; @@ -672,9 +680,13 @@ ); LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = "com.gertrude-skunk.ios-poc.controller"; + PRODUCT_BUNDLE_IDENTIFIER = "com.netrivet.gertrude-ios.app.controller"; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; @@ -704,9 +716,13 @@ ); LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = "com.gertrude-skunk.ios-poc.controller"; + PRODUCT_BUNDLE_IDENTIFIER = "com.netrivet.gertrude-ios.app.controller"; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; @@ -716,7 +732,7 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ - 7FA7B83E2AEB238700363B53 /* Build configuration list for PBXProject "ios-poc" */ = { + 7FA7B83E2AEB238700363B53 /* Build configuration list for PBXProject "Gertrude-iOS" */ = { isa = XCConfigurationList; buildConfigurations = ( 7FA7B84F2AEB238800363B53 /* Debug */, @@ -725,7 +741,7 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - 7FA7B8512AEB238800363B53 /* Build configuration list for PBXNativeTarget "ios-poc" */ = { + 7FA7B8512AEB238800363B53 /* Build configuration list for PBXNativeTarget "app" */ = { isa = XCConfigurationList; buildConfigurations = ( 7FA7B8522AEB238800363B53 /* Debug */, @@ -762,13 +778,14 @@ /* End XCLocalSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ - 7F7E54B52CB576B20012844E /* Filter */ = { + 7FEB782B2CC0058500A76A98 /* LibFilter */ = { isa = XCSwiftPackageProductDependency; - productName = Filter; + package = 7FFB548F2CB477C200EBFA3B /* XCLocalSwiftPackageReference "lib-ios" */; + productName = LibFilter; }; - 7FFB54902CB477C200EBFA3B /* App */ = { + 7FEC01B62CBF0DCA00B152B8 /* LibIOS */ = { isa = XCSwiftPackageProductDependency; - productName = App; + productName = LibIOS; }; /* End XCSwiftPackageProductDependency section */ }; diff --git a/iosapp/ios-poc.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/iosapp/Gertrude-iOS.xcodeproj/project.xcworkspace/contents.xcworkspacedata similarity index 100% rename from iosapp/ios-poc.xcodeproj/project.xcworkspace/contents.xcworkspacedata rename to iosapp/Gertrude-iOS.xcodeproj/project.xcworkspace/contents.xcworkspacedata diff --git a/iosapp/ios-poc.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/iosapp/Gertrude-iOS.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist similarity index 100% rename from iosapp/ios-poc.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist rename to iosapp/Gertrude-iOS.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist diff --git a/iosapp/ios-poc.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/iosapp/Gertrude-iOS.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved similarity index 81% rename from iosapp/ios-poc.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved rename to iosapp/Gertrude-iOS.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index b41a3b70..9efc9624 100644 --- a/iosapp/ios-poc.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/iosapp/Gertrude-iOS.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "294cc5392bc6679434856a5c441a94b895b5c278e5f25fa185fd58083dcee459", + "originHash" : "e63dc6b23ee4c31b02d892ff2ffcb166c0419168b7833fd893d2bf7072b738dd", "pins" : [ { "identity" : "combine-schedulers", @@ -42,8 +42,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-composable-architecture", "state" : { - "revision" : "8013f1a72af8ccb2b1735d7aed831a8dc07c6fd0", - "version" : "1.15.0" + "revision" : "fc5cbeec88114ff987f6c3cad3a7f3a3713fdb56", + "version" : "1.15.1" } }, { @@ -87,8 +87,17 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-navigation", "state" : { - "revision" : "d1bdbd8a5d1d1dfd2e4bb1f5e2f6facb631404d4", - "version" : "2.2.1" + "revision" : "16a27ab7ae0abfefbbcba73581b3e2380b47a579", + "version" : "2.2.2" + } + }, + { + "identity" : "swift-parsing", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-parsing", + "state" : { + "revision" : "a0e7d73f462c1c38c59dc40a3969ac40cea42950", + "version" : "0.13.0" } }, { @@ -109,6 +118,15 @@ "version" : "600.0.1" } }, + { + "identity" : "swift-url-routing", + "kind" : "remoteSourceControl", + "location" : "https://github.com/gertrude-app/swift-url-routing", + "state" : { + "branch" : "1cf1ca6", + "revision" : "1cf1ca67f4a4e442a599473e320049a85cd31588" + } + }, { "identity" : "xctest-dynamic-overlay", "kind" : "remoteSourceControl", diff --git a/iosapp/ios-poc.xcodeproj/xcshareddata/xcschemes/ios-poc.xcscheme b/iosapp/Gertrude-iOS.xcodeproj/xcshareddata/xcschemes/Gertrude-iOS.xcscheme similarity index 78% rename from iosapp/ios-poc.xcodeproj/xcshareddata/xcschemes/ios-poc.xcscheme rename to iosapp/Gertrude-iOS.xcodeproj/xcshareddata/xcschemes/Gertrude-iOS.xcscheme index 1e636f07..fb4ac19f 100644 --- a/iosapp/ios-poc.xcodeproj/xcshareddata/xcschemes/ios-poc.xcscheme +++ b/iosapp/Gertrude-iOS.xcodeproj/xcshareddata/xcschemes/Gertrude-iOS.xcscheme @@ -16,9 +16,9 @@ + BuildableName = "app.app" + BlueprintName = "app" + ReferencedContainer = "container:Gertrude-iOS.xcodeproj"> @@ -45,18 +45,11 @@ + BuildableName = "app.app" + BlueprintName = "app" + ReferencedContainer = "container:Gertrude-iOS.xcodeproj"> - - - - + BuildableName = "app.app" + BlueprintName = "app" + ReferencedContainer = "container:Gertrude-iOS.xcodeproj"> diff --git a/iosapp/Gertrude-iOS.xcodeproj/xcshareddata/xcschemes/controller.xcscheme b/iosapp/Gertrude-iOS.xcodeproj/xcshareddata/xcschemes/controller.xcscheme new file mode 100644 index 00000000..3b83c499 --- /dev/null +++ b/iosapp/Gertrude-iOS.xcodeproj/xcshareddata/xcschemes/controller.xcscheme @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/iosapp/ios-poc.xcodeproj/xcshareddata/xcschemes/filter.xcscheme b/iosapp/Gertrude-iOS.xcodeproj/xcshareddata/xcschemes/filter.xcscheme similarity index 100% rename from iosapp/ios-poc.xcodeproj/xcshareddata/xcschemes/filter.xcscheme rename to iosapp/Gertrude-iOS.xcodeproj/xcshareddata/xcschemes/filter.xcscheme diff --git a/iosapp/ios-poc/Assets.xcassets/AccentColor.colorset/Contents.json b/iosapp/app/Assets.xcassets/AccentColor.colorset/Contents.json similarity index 100% rename from iosapp/ios-poc/Assets.xcassets/AccentColor.colorset/Contents.json rename to iosapp/app/Assets.xcassets/AccentColor.colorset/Contents.json diff --git a/iosapp/ios-poc/Assets.xcassets/AppIcon.appiconset/Contents.json b/iosapp/app/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from iosapp/ios-poc/Assets.xcassets/AppIcon.appiconset/Contents.json rename to iosapp/app/Assets.xcassets/AppIcon.appiconset/Contents.json diff --git a/iosapp/ios-poc/Assets.xcassets/AppIcon.appiconset/icon-large.png b/iosapp/app/Assets.xcassets/AppIcon.appiconset/icon-large.png similarity index 100% rename from iosapp/ios-poc/Assets.xcassets/AppIcon.appiconset/icon-large.png rename to iosapp/app/Assets.xcassets/AppIcon.appiconset/icon-large.png diff --git a/iosapp/ios-poc/Assets.xcassets/Contents.json b/iosapp/app/Assets.xcassets/Contents.json similarity index 100% rename from iosapp/ios-poc/Assets.xcassets/Contents.json rename to iosapp/app/Assets.xcassets/Contents.json diff --git a/iosapp/ios-poc/Assets.xcassets/GertrudeIcon.imageset/Contents.json b/iosapp/app/Assets.xcassets/GertrudeIcon.imageset/Contents.json similarity index 100% rename from iosapp/ios-poc/Assets.xcassets/GertrudeIcon.imageset/Contents.json rename to iosapp/app/Assets.xcassets/GertrudeIcon.imageset/Contents.json diff --git a/iosapp/ios-poc/Assets.xcassets/GertrudeIcon.imageset/gertrude-icon1x.png b/iosapp/app/Assets.xcassets/GertrudeIcon.imageset/gertrude-icon1x.png similarity index 100% rename from iosapp/ios-poc/Assets.xcassets/GertrudeIcon.imageset/gertrude-icon1x.png rename to iosapp/app/Assets.xcassets/GertrudeIcon.imageset/gertrude-icon1x.png diff --git a/iosapp/ios-poc/Assets.xcassets/GertrudeIcon.imageset/gertrude-icon2x.png b/iosapp/app/Assets.xcassets/GertrudeIcon.imageset/gertrude-icon2x.png similarity index 100% rename from iosapp/ios-poc/Assets.xcassets/GertrudeIcon.imageset/gertrude-icon2x.png rename to iosapp/app/Assets.xcassets/GertrudeIcon.imageset/gertrude-icon2x.png diff --git a/iosapp/ios-poc/Assets.xcassets/GertrudeIcon.imageset/gertrude-icon3x.png b/iosapp/app/Assets.xcassets/GertrudeIcon.imageset/gertrude-icon3x.png similarity index 100% rename from iosapp/ios-poc/Assets.xcassets/GertrudeIcon.imageset/gertrude-icon3x.png rename to iosapp/app/Assets.xcassets/GertrudeIcon.imageset/gertrude-icon3x.png diff --git a/iosapp/ios-poc/ios_pocApp.swift b/iosapp/app/IOSAppEntry.swift similarity index 89% rename from iosapp/ios-poc/ios_pocApp.swift rename to iosapp/app/IOSAppEntry.swift index dbc10fbb..a6df5fa2 100644 --- a/iosapp/ios-poc/ios_pocApp.swift +++ b/iosapp/app/IOSAppEntry.swift @@ -1,9 +1,9 @@ -import App import ComposableArchitecture +import LibIOS import SwiftUI @main -struct ios_pocApp: App { +struct IOSAppEntry: App { let store: StoreOf init() { diff --git a/iosapp/ios-poc/Info.plist b/iosapp/app/Info.plist similarity index 86% rename from iosapp/ios-poc/Info.plist rename to iosapp/app/Info.plist index fa221f2d..8146926c 100644 --- a/iosapp/ios-poc/Info.plist +++ b/iosapp/app/Info.plist @@ -2,12 +2,12 @@ - UILaunchScreen - + UILaunchScreen + UIColorName white UIImageName GertrudeIcon - + diff --git a/iosapp/ios-poc/Preview Content/Preview Assets.xcassets/Contents.json b/iosapp/app/Preview Content/Preview Assets.xcassets/Contents.json similarity index 100% rename from iosapp/ios-poc/Preview Content/Preview Assets.xcassets/Contents.json rename to iosapp/app/Preview Content/Preview Assets.xcassets/Contents.json diff --git a/iosapp/app/app.entitlements b/iosapp/app/app.entitlements new file mode 100644 index 00000000..31b7058b --- /dev/null +++ b/iosapp/app/app.entitlements @@ -0,0 +1,16 @@ + + + + + com.apple.developer.family-controls + + com.apple.developer.networking.networkextension + + content-filter-provider + + com.apple.security.application-groups + + group.com.netrivet.gertrude-ios.app + + + diff --git a/iosapp/controller/controller.entitlements b/iosapp/controller/controller.entitlements index cbc0897a..462251b3 100644 --- a/iosapp/controller/controller.entitlements +++ b/iosapp/controller/controller.entitlements @@ -6,9 +6,9 @@ content-filter-provider - com.apple.security.application-groups - - group.com.gertrude-skunk.ios-poc - + com.apple.security.application-groups + + group.com.netrivet.gertrude-ios.app + diff --git a/iosapp/filter/FilterDataProvider.swift b/iosapp/filter/FilterDataProvider.swift index 658bc450..5305f93e 100644 --- a/iosapp/filter/FilterDataProvider.swift +++ b/iosapp/filter/FilterDataProvider.swift @@ -1,4 +1,4 @@ -import Filter +import LibFilter import NetworkExtension import os.log diff --git a/iosapp/filter/filter.entitlements b/iosapp/filter/filter.entitlements index cbc0897a..462251b3 100644 --- a/iosapp/filter/filter.entitlements +++ b/iosapp/filter/filter.entitlements @@ -6,9 +6,9 @@ content-filter-provider - com.apple.security.application-groups - - group.com.gertrude-skunk.ios-poc - + com.apple.security.application-groups + + group.com.netrivet.gertrude-ios.app + diff --git a/iosapp/ios-poc/ios-poc.entitlements b/iosapp/ios-poc/ios-poc.entitlements deleted file mode 100644 index 0f42515e..00000000 --- a/iosapp/ios-poc/ios-poc.entitlements +++ /dev/null @@ -1,16 +0,0 @@ - - - - - com.apple.developer.family-controls - - com.apple.developer.networking.networkextension - - content-filter-provider - - com.apple.security.application-groups - - group.com.gertrude-skunk.ios-poc - - - diff --git a/iosapp/lib-ios/Package.resolved b/iosapp/lib-ios/Package.resolved index ab0da880..fdb4be1d 100644 --- a/iosapp/lib-ios/Package.resolved +++ b/iosapp/lib-ios/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "7cd15ad1fa8aaf83ad148fa4bb78ed2b0b9608033ad6e39929da31d436a1ee24", + "originHash" : "f3a15af06196a2299b9cc4d8b5cea622813d35446bd8878e8b16ad6870196668", "pins" : [ { "identity" : "combine-schedulers", @@ -42,8 +42,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-composable-architecture", "state" : { - "revision" : "8013f1a72af8ccb2b1735d7aed831a8dc07c6fd0", - "version" : "1.15.0" + "revision" : "fc5cbeec88114ff987f6c3cad3a7f3a3713fdb56", + "version" : "1.15.1" } }, { @@ -87,8 +87,17 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-navigation", "state" : { - "revision" : "d1bdbd8a5d1d1dfd2e4bb1f5e2f6facb631404d4", - "version" : "2.2.1" + "revision" : "16a27ab7ae0abfefbbcba73581b3e2380b47a579", + "version" : "2.2.2" + } + }, + { + "identity" : "swift-parsing", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-parsing", + "state" : { + "revision" : "a0e7d73f462c1c38c59dc40a3969ac40cea42950", + "version" : "0.13.0" } }, { @@ -109,6 +118,15 @@ "version" : "600.0.1" } }, + { + "identity" : "swift-url-routing", + "kind" : "remoteSourceControl", + "location" : "https://github.com/gertrude-app/swift-url-routing", + "state" : { + "branch" : "1cf1ca6", + "revision" : "1cf1ca67f4a4e442a599473e320049a85cd31588" + } + }, { "identity" : "xctest-dynamic-overlay", "kind" : "remoteSourceControl", diff --git a/iosapp/lib-ios/Package.swift b/iosapp/lib-ios/Package.swift index a3556daa..6c127208 100644 --- a/iosapp/lib-ios/Package.swift +++ b/iosapp/lib-ios/Package.swift @@ -1,38 +1,39 @@ // swift-tools-version: 5.10 - import PackageDescription let package = Package( - name: "App", - platforms: [.macOS(.v13), .iOS(.v17)], + name: "LibIOS", + platforms: [.macOS(.v14), .iOS(.v17)], products: [ - .library(name: "App", targets: ["App"]), - .library(name: "Filter", targets: ["Filter"]), + .library(name: "LibIOS", targets: ["LibIOS"]), + .library(name: "LibFilter", targets: ["LibFilter"]), ], dependencies: [ .package( url: "https://github.com/pointfreeco/swift-composable-architecture", from: "1.0.0" ), + .package(path: "../../pairql-iosapp"), ], targets: [ .target( - name: "App", + name: "LibIOS", dependencies: [ .product(name: "ComposableArchitecture", package: "swift-composable-architecture"), + .product(name: "IOSRoute", package: "pairql-iosapp"), ] ), .target( - name: "Filter", + name: "LibFilter", dependencies: [] ), .testTarget( - name: "AppTests", - dependencies: ["App"] + name: "LibIOSTests", + dependencies: ["LibIOS"] ), .testTarget( - name: "FilterTests", - dependencies: ["Filter"] + name: "LibFilterTests", + dependencies: ["LibFilter"] ), ] ) diff --git a/iosapp/lib-ios/Sources/App/App+Install.swift b/iosapp/lib-ios/Sources/App/App+Install.swift deleted file mode 100644 index 64b6e1b0..00000000 --- a/iosapp/lib-ios/Sources/App/App+Install.swift +++ /dev/null @@ -1,130 +0,0 @@ -import FamilyControls -import NetworkExtension -import os.log - -// @see https://developer.apple.com/documentation/familycontrols/familycontrolserror -public enum AuthFailureReason: Error, Equatable { - // The device isn't signed into a valid iCloud account (also? .individual?) - case invalidAccountType - /// Another authorized app already provides parental controls - case authorizationConflict - case unexpected(Unexpected) - case other(String) - /// Device must be connected to the network in order to enroll with parental controls - case networkError - /// The device must have a passcode set in order for an individual to enroll with parental controls - case passcodeRequired - /// The parent or guardian cancelled a request for authorization - case authorizationCanceled - - public enum Unexpected: Equatable { - /// The method's arguments are invalid - case invalidArgument - /// The system failed to set up the Family Control famework - case unavailable - /// A restriction prevents your app from using Family Controls on this device - case restricted - } -} - -public enum FilterInstallError: Error, Equatable { - case configurationInvalid - case configurationDisabled - /// another process modified the filter configuration - /// since the last time the app loaded the configuration - case configurationStale - /// removing the configuration isn't allowed - case configurationCannotBeRemoved - case configurationPermissionDenied - case configurationInternalError - case unexpected(String) -} - -// TODO: extract into @Dependency -func requestAuthorization() async -> Result { - // TODO: figure out SPM things... - #if os(iOS) - do { - #if DEBUG - try await AuthorizationCenter.shared.requestAuthorization(for: .individual) - #else - try await AuthorizationCenter.shared.requestAuthorization(for: .child) - #endif - } catch let familyError as FamilyControlsError { - switch familyError { - case .invalidAccountType: - return .failure(.invalidAccountType) - case .authorizationConflict: - return .failure(.authorizationConflict) - case .authorizationCanceled: - return .failure(.authorizationCanceled) - case .networkError: - return .failure(.networkError) - case .authenticationMethodUnavailable: - return .failure(.passcodeRequired) - case .restricted: - return .failure(.unexpected(.restricted)) - case .unavailable: - return .failure(.unexpected(.unavailable)) - case .invalidArgument: - return .failure(.unexpected(.invalidArgument)) - @unknown default: - return .failure(.other(String(reflecting: familyError))) - } - } catch { - return .failure(.other(String(reflecting: error))) - } - #endif - return .success(()) -} - -func saveConfiguration() async -> Result { - // not sure this is necessary, but doesn't seem to hurt and might ensure clean slate - try? await NEFilterManager.shared().removeFromPreferences() - - if NEFilterManager.shared().providerConfiguration == nil { - let newConfiguration = NEFilterProviderConfiguration() - newConfiguration.username = "IOSPoc" - newConfiguration.organization = "GertrudeSkunk" - #if os(iOS) - newConfiguration.filterBrowsers = true - #endif - newConfiguration.filterSockets = true - NEFilterManager.shared().providerConfiguration = newConfiguration - } - NEFilterManager.shared().isEnabled = true - do { - try await NEFilterManager.shared().saveToPreferences() - return .success(()) - } catch { - switch NEFilterManagerError(rawValue: (error as NSError).code) { - case .some(.configurationInvalid): - return .failure(.configurationInvalid) - case .some(.configurationDisabled): - return .failure(.configurationDisabled) - case .some(.configurationStale): - return .failure(.configurationStale) - case .some(.configurationCannotBeRemoved): - return .failure(.configurationCannotBeRemoved) - case .some(.configurationPermissionDenied): - return .failure(.configurationPermissionDenied) - case .some(.configurationInternalError): - return .failure(.configurationInternalError) - case .none: - return .failure(.unexpected(String(reflecting: error))) - @unknown default: - return .failure(.unexpected(String(reflecting: error))) - } - } -} - -func isRunning() async -> Bool { - do { - try await NEFilterManager.shared().loadFromPreferences() - return NEFilterManager.shared().isEnabled - } catch { - print("Error loading preferences: \(error)") - os_log("[G•] error loading preferences (isRunning()): %{public}s", String(reflecting: error)) - return false - } -} diff --git a/iosapp/lib-ios/Sources/App/Lib/Colors.swift b/iosapp/lib-ios/Sources/App/Lib/Colors.swift deleted file mode 100644 index ec1b8f47..00000000 --- a/iosapp/lib-ios/Sources/App/Lib/Colors.swift +++ /dev/null @@ -1,52 +0,0 @@ -import SwiftUI - -let violet100 = Color(hex: "#ede9fe")! -let violet200 = Color(hex: "#ddd6fe")! -let violet300 = Color(hex: "#c4b5fd")! -let violet400 = Color(hex: "#a78bfa")! -let violet500 = Color(hex: "#8b5cf6")! -let violet600 = Color(hex: "#7c3aed")! -let violet700 = Color(hex: "#6d28d9")! -let violet800 = Color(hex: "#5b21b6")! -let violet900 = Color(hex: "#4c1d95")! - -let fuschia100 = Color(hex: "#fae8ff")! -let fuchsia200 = Color(hex: "#f5d0fe")! -let fuchsia300 = Color(hex: "#f0abfc")! -let fuchsia400 = Color(hex: "#e879f9")! -let fuchsia500 = Color(hex: "#d946ef")! -let fuchsia600 = Color(hex: "#c026d3")! -let fuchsia700 = Color(hex: "#a21caf")! -let fuchsia800 = Color(hex: "#86198f")! -let fuchsia900 = Color(hex: "#701a75")! - -public extension Color { - init?(hex: String) { - let r, g, b: Double - - var hexSanitized = hex.trimmingCharacters(in: .whitespacesAndNewlines) - hexSanitized = hexSanitized.hasPrefix("#") ? String(hexSanitized.dropFirst()) : hexSanitized - - var rgb: UInt64 = 0 - Scanner(string: hexSanitized).scanHexInt64(&rgb) - - switch hexSanitized.count { - case 3: // RGB - (r, g, b) = ( - Double((rgb >> 8) & 0xF) / 15.0, - Double((rgb >> 4) & 0xF) / 15.0, - Double(rgb & 0xF) / 15.0 - ) - case 6: // RRGGBB - (r, g, b) = ( - Double((rgb >> 16) & 0xFF) / 255.0, - Double((rgb >> 8) & 0xFF) / 255.0, - Double(rgb & 0xFF) / 255.0 - ) - default: - return nil - } - - self.init(red: r, green: g, blue: b) - } -} diff --git a/iosapp/lib-ios/Sources/App/Views/BgGradient.swift b/iosapp/lib-ios/Sources/App/Views/BgGradient.swift deleted file mode 100644 index 6d14788b..00000000 --- a/iosapp/lib-ios/Sources/App/Views/BgGradient.swift +++ /dev/null @@ -1,50 +0,0 @@ -import SwiftUI - -struct BgGradient: View { - @State var t: CGFloat = 0 - @State var timer: Timer? - - var body: some View { - if #available(iOS 18.0, *) { - MeshGradient( - width: 3, - height: 4, - points: [ - [0, 0], [0.5, 0], [1, 0], - [0, 0.33], [Float(cos(self.t) / 2.5 + 0.5), Float(sin(2 * self.t) / 6 + 0.33)], [1, 0.5], - [0, 0.66], [Float(-cos(self.t) / 2.5 + 0.5), Float(-sin(self.t) / 4 + 0.66)], [1, 0.5], - [0, 1], [0.5, 1], [1, 1], - ], - colors: [ - fuchsia300, violet300, .white, - .white, .white, fuchsia300, - violet100, fuchsia300, violet300, - fuchsia300, violet300, violet100, - ], - smoothsColors: true - ).onAppear { - self.startAnimation() - }.onDisappear { - self.stopAnimation() - } - } else { - Rectangle().fill(Gradient(colors: [.white, violet300])) - } - } - - private func startAnimation() { - self.timer = Timer.scheduledTimer( - withTimeInterval: 0.016, - repeats: true - ) { _ in - withAnimation(.linear(duration: 0.016)) { - self.t += 0.005 - } - } - } - - private func stopAnimation() { - self.timer?.invalidate() - self.timer = nil - } -} diff --git a/iosapp/lib-ios/Sources/Filter/Filter.swift b/iosapp/lib-ios/Sources/LibFilter/Filter.swift similarity index 67% rename from iosapp/lib-ios/Sources/Filter/Filter.swift rename to iosapp/lib-ios/Sources/LibFilter/Filter.swift index fa448142..2db93cdd 100644 --- a/iosapp/lib-ios/Sources/Filter/Filter.swift +++ b/iosapp/lib-ios/Sources/LibFilter/Filter.swift @@ -3,6 +3,8 @@ public func decideFlow(hostname: String?, url: String?, sourceId: String?) -> Bo return false } else if sourceId?.contains("com.apple.Spotlight") == true { return false + } else if sourceId?.contains(".com.apple.photoanalysisd") == true { + return false } if url?.contains("tenor.co") == true { @@ -18,6 +20,12 @@ public func decideFlow(hostname: String?, url: String?, sourceId: String?) -> Bo return false } else if target.contains("media.fosu2-1.fna.whatsapp.net") { return false + } else if sourceId?.contains(".com.apple.MobileSMS") == true { + if target.contains("amp-api-edge.apps.apple.com") { + return false + } else if target.contains("is1-ssl.mzstatic.com") { + return false + } } } return true diff --git a/iosapp/lib-ios/Sources/LibIOS/ApiClient.swift b/iosapp/lib-ios/Sources/LibIOS/ApiClient.swift new file mode 100644 index 00000000..811ff9e6 --- /dev/null +++ b/iosapp/lib-ios/Sources/LibIOS/ApiClient.swift @@ -0,0 +1,45 @@ +import Dependencies +import DependenciesMacros +import Foundation +import IOSRoute +import os.log + +@DependencyClient +struct ApiClient: Sendable { + var logEvent: @Sendable (_ id: String, _ detail: String?) async -> Void +} + +extension ApiClient: TestDependencyKey { + public static let testValue = ApiClient() +} + +extension ApiClient: DependencyKey { + public static var liveValue: ApiClient { + ApiClient { id, detail in + let payload = LogIOSEvent.Input( + eventId: id, + kind: "ios", + deviceType: Device.current.type, + iOSVersion: Device.current.iOSVersion, + vendorId: Device.current.vendorId, + detail: detail + ) + do { + let router = IOSRoute.router.baseURL(.gertrudeApi) + var request = try router.request(for: .logIOSEvent(payload)) + request.cachePolicy = .reloadIgnoringLocalAndRemoteCacheData + request.httpMethod = "POST" + _ = try await URLSession.shared.data(for: request) + } catch { + os_log("[G•] error logging event: %{public}s", String(reflecting: error)) + } + } + } +} + +extension DependencyValues { + var api: ApiClient { + get { self[ApiClient.self] } + set { self[ApiClient.self] = newValue } + } +} diff --git a/iosapp/lib-ios/Sources/LibIOS/App+Types.swift b/iosapp/lib-ios/Sources/LibIOS/App+Types.swift new file mode 100644 index 00000000..1595d484 --- /dev/null +++ b/iosapp/lib-ios/Sources/LibIOS/App+Types.swift @@ -0,0 +1,81 @@ +import FamilyControls +import IOSRoute +import NetworkExtension +import os.log + +#if os(iOS) + import UIKit +#endif + +struct Device { + var type: String + var iOSVersion: String + var vendorId: UUID? +} + +extension Device { + static var current: Device { + #if os(iOS) + Device( + type: UIDevice.current.userInterfaceIdiom == .pad ? "iPad" : "iPhone", + iOSVersion: UIDevice.current.systemVersion, + vendorId: UIDevice.current.identifierForVendor + ) + #else + Device(type: "iPhone", iOSVersion: "18.0.1", vendorId: nil) + #endif + } +} + +// @see https://developer.apple.com/documentation/familycontrols/familycontrolserror +public enum AuthFailureReason: Error, Equatable { + // The device isn't signed into a valid iCloud account (also? .individual?) + case invalidAccountType + /// Another authorized app already provides parental controls + case authorizationConflict + case unexpected(Unexpected) + case other(String) + /// Device must be connected to the network in order to enroll with parental controls + case networkError + /// The device must have a passcode set in order for an individual to enroll with parental controls + case passcodeRequired + /// The parent or guardian cancelled a request for authorization + case authorizationCanceled + + public enum Unexpected: Equatable { + /// The method's arguments are invalid + case invalidArgument + /// The system failed to set up the Family Control famework + case unavailable + /// A restriction prevents your app from using Family Controls on this device + case restricted + } +} + +public enum FilterInstallError: Error, Equatable { + case configurationInvalid + case configurationDisabled + /// another process modified the filter configuration + /// since the last time the app loaded the configuration + case configurationStale + /// removing the configuration isn't allowed + case configurationCannotBeRemoved + case configurationPermissionDenied + case configurationInternalError + case unexpected(String) +} + +extension String { + static var gertrudeApi: String { + #if DEBUG + // just run-api-ip + return "http://192.168.10.227:8080/pairql/ios-app" + #else + return "https://api.gertrude.app/pairql/ios-app" + #endif + } + + static var launchDateStorageKey: String { + "firstLaunchDate" + } +} diff --git a/iosapp/lib-ios/Sources/App/App.swift b/iosapp/lib-ios/Sources/LibIOS/App.swift similarity index 65% rename from iosapp/lib-ios/Sources/App/App.swift rename to iosapp/lib-ios/Sources/LibIOS/App.swift index a0a8c74a..70495dd2 100644 --- a/iosapp/lib-ios/Sources/App/App.swift +++ b/iosapp/lib-ios/Sources/LibIOS/App.swift @@ -1,16 +1,27 @@ import ComposableArchitecture +import Foundation @Reducer public struct AppReducer { @ObservableState public struct State: Equatable { public var appState: AppState + public var firstLaunch: Date? public init(appState: AppState = .launching) { self.appState = appState } } + @ObservationIgnored + @Dependency(\.api) var api + @ObservationIgnored + @Dependency(\.system) var system + @ObservationIgnored + @Dependency(\.storage) var storage + @ObservationIgnored + @Dependency(\.date.now) var now + // TODO: figure out why i can't use a root store enum public enum AppState: Equatable { case launching @@ -37,6 +48,7 @@ public struct AppReducer { case installFilterTapped case postInstallOkTapped case setRunning(Bool) + case setFirstLaunch(Date) } public var body: some Reducer { @@ -45,13 +57,25 @@ public struct AppReducer { case .appLaunched: return .run { send in - await send(.setRunning(await isRunning())) + await send(.setRunning(await self.system.filterRunning())) + if let firstLaunch = self.storage.object(forKey: .launchDateStorageKey) as? Date { + await send(.setFirstLaunch(firstLaunch)) + } else { + let now = self.now + self.storage.set(now, forKey: .launchDateStorageKey) + await send(.setFirstLaunch(now)) + await self.api.logEvent("dcd721aa", "first launch") + } } case .setRunning(true): state.appState = .running return .none + case .setFirstLaunch(let date): + state.firstLaunch = date + return .none + case .setRunning(false): state.appState = .welcome return .none @@ -63,11 +87,14 @@ public struct AppReducer { case .startAuthorizationTapped: state.appState = .authorizing return .run { send in - switch await requestAuthorization() { + switch await self.system.requestAuthorization() { case .success: await send(.authorizationSucceeded) + await self.api.logEvent("d317c73c", "authorization succeeded") case .failure(let reason): await send(.authorizationFailed(reason)) + await self.system.cleanupForRetry() + await self.api.logEvent("d9dfd021", "authorization failed: \(reason)") } } @@ -85,11 +112,14 @@ public struct AppReducer { case .installFilterTapped: return .run { send in - switch await saveConfiguration() { + switch await self.system.installFilter() { case .success: await send(.installSucceeded) + await self.api.logEvent("101c91ea", "filter install success") case .failure(let error): await send(.installFailed(error)) + await self.system.cleanupForRetry() + await self.api.logEvent("739c08c6", "filter install failed: \(error)") } } @@ -98,7 +128,7 @@ public struct AppReducer { return .none case .installFailedTryAgainTapped: - // TODO: clean up for retry + state.appState = .welcome return .none case .installSucceeded: diff --git a/iosapp/lib-ios/Sources/App/ContentView.swift b/iosapp/lib-ios/Sources/LibIOS/ContentView.swift similarity index 94% rename from iosapp/lib-ios/Sources/App/ContentView.swift rename to iosapp/lib-ios/Sources/LibIOS/ContentView.swift index 934330f1..afec0b31 100644 --- a/iosapp/lib-ios/Sources/App/ContentView.swift +++ b/iosapp/lib-ios/Sources/LibIOS/ContentView.swift @@ -59,11 +59,7 @@ public struct ContentView: View { public extension View { var deviceType: String { - #if os(iOS) - UIDevice.current.userInterfaceIdiom == .pad ? "iPad" : "iPhone" - #else - "iPhone" - #endif + Device.current.type } } diff --git a/iosapp/lib-ios/Sources/LibIOS/Lib/Colors.swift b/iosapp/lib-ios/Sources/LibIOS/Lib/Colors.swift new file mode 100644 index 00000000..1ed77ef8 --- /dev/null +++ b/iosapp/lib-ios/Sources/LibIOS/Lib/Colors.swift @@ -0,0 +1,52 @@ +import SwiftUI + +public extension Color { + static let violet100 = Color(hex: "#ede9fe")! + static let violet200 = Color(hex: "#ddd6fe")! + static let violet300 = Color(hex: "#c4b5fd")! + static let violet400 = Color(hex: "#a78bfa")! + static let violet500 = Color(hex: "#8b5cf6")! + static let violet600 = Color(hex: "#7c3aed")! + static let violet700 = Color(hex: "#6d28d9")! + static let violet800 = Color(hex: "#5b21b6")! + static let violet900 = Color(hex: "#4c1d95")! + + static let fuschia100 = Color(hex: "#fae8ff")! + static let fuchsia200 = Color(hex: "#f5d0fe")! + static let fuchsia300 = Color(hex: "#f0abfc")! + static let fuchsia400 = Color(hex: "#e879f9")! + static let fuchsia500 = Color(hex: "#d946ef")! + static let fuchsia600 = Color(hex: "#c026d3")! + static let fuchsia700 = Color(hex: "#a21caf")! + static let fuchsia800 = Color(hex: "#86198f")! + static let fuchsia900 = Color(hex: "#701a75")! + + init?(hex: String) { + let r, g, b: Double + + var hexSanitized = hex.trimmingCharacters(in: .whitespacesAndNewlines) + hexSanitized = hexSanitized.hasPrefix("#") ? String(hexSanitized.dropFirst()) : hexSanitized + + var rgb: UInt64 = 0 + Scanner(string: hexSanitized).scanHexInt64(&rgb) + + switch hexSanitized.count { + case 3: // RGB + (r, g, b) = ( + Double((rgb >> 8) & 0xF) / 15.0, + Double((rgb >> 4) & 0xF) / 15.0, + Double(rgb & 0xF) / 15.0 + ) + case 6: // RRGGBB + (r, g, b) = ( + Double((rgb >> 16) & 0xFF) / 255.0, + Double((rgb >> 8) & 0xFF) / 255.0, + Double(rgb & 0xFF) / 255.0 + ) + default: + return nil + } + + self.init(red: r, green: g, blue: b) + } +} diff --git a/iosapp/lib-ios/Sources/LibIOS/StorageClient.swift b/iosapp/lib-ios/Sources/LibIOS/StorageClient.swift new file mode 100644 index 00000000..74bc66d2 --- /dev/null +++ b/iosapp/lib-ios/Sources/LibIOS/StorageClient.swift @@ -0,0 +1,31 @@ +import Dependencies +import DependenciesMacros +import Foundation + +@DependencyClient +struct StorageClient: Sendable { + var object: @Sendable (_ forKey: String) -> Any? + var set: @Sendable (Any?, _ forKey: String) -> Void +} + +extension StorageClient: DependencyKey { + public static let liveValue = StorageClient( + object: { + key in UserDefaults.standard.object(forKey: key) + }, + set: { value, key in + UserDefaults.standard.set(value, forKey: key) + } + ) +} + +extension StorageClient: TestDependencyKey { + public static let testValue = StorageClient() +} + +extension DependencyValues { + var storage: StorageClient { + get { self[StorageClient.self] } + set { self[StorageClient.self] = newValue } + } +} diff --git a/iosapp/lib-ios/Sources/LibIOS/SystemClient.swift b/iosapp/lib-ios/Sources/LibIOS/SystemClient.swift new file mode 100644 index 00000000..26f4cfbb --- /dev/null +++ b/iosapp/lib-ios/Sources/LibIOS/SystemClient.swift @@ -0,0 +1,125 @@ +import Dependencies +import FamilyControls +import NetworkExtension +import os.log + +struct SystemClient: Sendable { + var requestAuthorization: @Sendable () async -> Result + var installFilter: @Sendable () async -> Result + var filterRunning: @Sendable () async -> Bool + var cleanupForRetry: @Sendable () async -> Void +} + +extension SystemClient: DependencyKey { + public static let liveValue = SystemClient( + requestAuthorization: { + #if os(iOS) + do { + #if DEBUG + try await AuthorizationCenter.shared.requestAuthorization(for: .individual) + #else + try await AuthorizationCenter.shared.requestAuthorization(for: .child) + #endif + } catch let familyError as FamilyControlsError { + switch familyError { + case .invalidAccountType: + return .failure(.invalidAccountType) + case .authorizationConflict: + return .failure(.authorizationConflict) + case .authorizationCanceled: + return .failure(.authorizationCanceled) + case .networkError: + return .failure(.networkError) + case .authenticationMethodUnavailable: + return .failure(.passcodeRequired) + case .restricted: + return .failure(.unexpected(.restricted)) + case .unavailable: + return .failure(.unexpected(.unavailable)) + case .invalidArgument: + return .failure(.unexpected(.invalidArgument)) + @unknown default: + return .failure(.other(String(reflecting: familyError))) + } + } catch { + return .failure(.other(String(reflecting: error))) + } + #endif + return .success(()) + }, + installFilter: { + // not sure this is necessary, but doesn't seem to hurt and might ensure clean slate + try? await NEFilterManager.shared().removeFromPreferences() + + if NEFilterManager.shared().providerConfiguration == nil { + let newConfiguration = NEFilterProviderConfiguration() + newConfiguration.username = "Gertrude" + newConfiguration.organization = "Gertrude" + #if os(iOS) + newConfiguration.filterBrowsers = true + #endif + newConfiguration.filterSockets = true + NEFilterManager.shared().providerConfiguration = newConfiguration + } + NEFilterManager.shared().isEnabled = true + do { + try await NEFilterManager.shared().saveToPreferences() + return .success(()) + } catch { + switch NEFilterManagerError(rawValue: (error as NSError).code) { + case .some(.configurationInvalid): + return .failure(.configurationInvalid) + case .some(.configurationDisabled): + return .failure(.configurationDisabled) + case .some(.configurationStale): + return .failure(.configurationStale) + case .some(.configurationCannotBeRemoved): + return .failure(.configurationCannotBeRemoved) + case .some(.configurationPermissionDenied): + return .failure(.configurationPermissionDenied) + case .some(.configurationInternalError): + return .failure(.configurationInternalError) + case .none: + return .failure(.unexpected(String(reflecting: error))) + @unknown default: + return .failure(.unexpected(String(reflecting: error))) + } + } + }, + filterRunning: { + do { + try await NEFilterManager.shared().loadFromPreferences() + return NEFilterManager.shared().isEnabled + } catch { + os_log( + "[G•] error loading preferences: %{public}s", + String(reflecting: error) + ) + return false + } + }, + cleanupForRetry: { + NEFilterManager.shared().providerConfiguration = nil + try? await NEFilterManager.shared().removeFromPreferences() + #if os(iOS) + AuthorizationCenter.shared.revokeAuthorization { _ in } + #endif + } + ) +} + +extension SystemClient: TestDependencyKey { + public static let testValue = SystemClient( + requestAuthorization: { .success(()) }, + installFilter: { .success(()) }, + filterRunning: { false }, + cleanupForRetry: {} + ) +} + +extension DependencyValues { + var system: SystemClient { + get { self[SystemClient.self] } + set { self[SystemClient.self] = newValue } + } +} diff --git a/iosapp/lib-ios/Sources/App/Views/AuthFailed.swift b/iosapp/lib-ios/Sources/LibIOS/Views/AuthFailed.swift similarity index 96% rename from iosapp/lib-ios/Sources/App/Views/AuthFailed.swift rename to iosapp/lib-ios/Sources/LibIOS/Views/AuthFailed.swift index 38cd4da7..21a06042 100644 --- a/iosapp/lib-ios/Sources/App/Views/AuthFailed.swift +++ b/iosapp/lib-ios/Sources/LibIOS/Views/AuthFailed.swift @@ -42,7 +42,6 @@ struct AuthFailed: View { } } .multilineTextAlignment(.center) - .font(.footnote) Button { self.onTryAgain() @@ -52,9 +51,9 @@ struct AuthFailed: View { Spacer() } .padding(.vertical, 12) - .background(violet100) + .background(Color.violet100) .cornerRadius(8) - .foregroundColor(violet700) + .foregroundColor(.violet700) .font(.system(size: 16, weight: .semibold)) } .padding(20) diff --git a/iosapp/lib-ios/Sources/App/Views/Authorized.swift b/iosapp/lib-ios/Sources/LibIOS/Views/Authorized.swift similarity index 92% rename from iosapp/lib-ios/Sources/App/Views/Authorized.swift rename to iosapp/lib-ios/Sources/LibIOS/Views/Authorized.swift index b3728e78..6a71d69a 100644 --- a/iosapp/lib-ios/Sources/App/Views/Authorized.swift +++ b/iosapp/lib-ios/Sources/LibIOS/Views/Authorized.swift @@ -17,9 +17,9 @@ struct Authorized: View { Spacer() } .padding(.vertical, 12) - .background(violet100) + .background(Color.violet100) .cornerRadius(8) - .foregroundColor(violet700) + .foregroundColor(.violet700) .font(.system(size: 16, weight: .semibold)) } .padding(20) diff --git a/iosapp/lib-ios/Sources/LibIOS/Views/BgGradient.swift b/iosapp/lib-ios/Sources/LibIOS/Views/BgGradient.swift new file mode 100644 index 00000000..ec471776 --- /dev/null +++ b/iosapp/lib-ios/Sources/LibIOS/Views/BgGradient.swift @@ -0,0 +1,56 @@ +import SwiftUI + +struct BgGradient: View { + @State var t: CGFloat = 0 + @State var timer: Timer? + + var body: some View { + if #available(iOS 18.0, *) { + #if os(iOS) + MeshGradient( + width: 3, + height: 4, + points: [ + [0, 0], [0.5, 0], [1, 0], + [0, 0.33], [Float(cos(self.t) / 2.5 + 0.5), Float(sin(2 * self.t) / 6 + 0.33)], + [1, 0.5], + [0, 0.66], [Float(-cos(self.t) / 2.5 + 0.5), Float(-sin(self.t) / 4 + 0.66)], [1, 0.5], + [0, 1], [0.5, 1], [1, 1], + ], + colors: [ + .fuchsia300, .violet300, .white, + .white, .white, .fuchsia300, + .violet100, .fuchsia300, .violet300, + .fuchsia300, .violet300, .violet100, + ], + smoothsColors: true + ).onAppear { + self.startAnimation() + }.onDisappear { + self.stopAnimation() + } + #else + // keep nvim LSP happy + Rectangle().fill(Gradient(colors: [.white, .violet300])) + #endif + } else { + Rectangle().fill(Gradient(colors: [.white, .violet300])) + } + } + + private func startAnimation() { + self.timer = Timer.scheduledTimer( + withTimeInterval: 0.016, + repeats: true + ) { _ in + withAnimation(.linear(duration: 0.016)) { + self.t += 0.005 + } + } + } + + private func stopAnimation() { + self.timer?.invalidate() + self.timer = nil + } +} diff --git a/iosapp/lib-ios/Sources/App/Views/FeatureLI.swift b/iosapp/lib-ios/Sources/LibIOS/Views/FeatureLI.swift similarity index 100% rename from iosapp/lib-ios/Sources/App/Views/FeatureLI.swift rename to iosapp/lib-ios/Sources/LibIOS/Views/FeatureLI.swift diff --git a/iosapp/lib-ios/Sources/App/Views/InstallFail.swift b/iosapp/lib-ios/Sources/LibIOS/Views/InstallFail.swift similarity index 96% rename from iosapp/lib-ios/Sources/App/Views/InstallFail.swift rename to iosapp/lib-ios/Sources/LibIOS/Views/InstallFail.swift index 7d173135..364dad27 100644 --- a/iosapp/lib-ios/Sources/App/Views/InstallFail.swift +++ b/iosapp/lib-ios/Sources/LibIOS/Views/InstallFail.swift @@ -26,7 +26,6 @@ struct InstallFail: View { Text("Unexpected error: \(underlying)") } } - .font(.footnote) .foregroundColor(.red) .padding(.bottom, 20) .multilineTextAlignment(.center) @@ -39,9 +38,9 @@ struct InstallFail: View { Spacer() } .padding(.vertical, 12) - .background(violet100) + .background(Color.violet100) .cornerRadius(8) - .foregroundColor(violet700) + .foregroundColor(.violet700) .font(.system(size: 16, weight: .semibold)) } .padding(20) diff --git a/iosapp/lib-ios/Sources/App/Views/LoadingScreen.swift b/iosapp/lib-ios/Sources/LibIOS/Views/LoadingScreen.swift similarity index 88% rename from iosapp/lib-ios/Sources/App/Views/LoadingScreen.swift rename to iosapp/lib-ios/Sources/LibIOS/Views/LoadingScreen.swift index e3f63710..0be663df 100644 --- a/iosapp/lib-ios/Sources/App/Views/LoadingScreen.swift +++ b/iosapp/lib-ios/Sources/LibIOS/Views/LoadingScreen.swift @@ -10,15 +10,15 @@ public struct LoadingScreen: View { Rectangle() .frame(width: -sin(self.t) * 5 + 15, height: sin(self.t) * 40 + 60) .cornerRadius(20) - .foregroundColor(violet500.opacity(0.8)) + .foregroundColor(.violet500.opacity(0.8)) Rectangle() .frame(width: -sin(self.t + 0.4) * 5 + 15, height: sin(self.t + 0.4) * 40 + 60) .cornerRadius(20) - .foregroundColor(violet500.opacity(0.8)) + .foregroundColor(.violet500.opacity(0.8)) Rectangle() .frame(width: -sin(self.t + 0.8) * 5 + 15, height: sin(self.t + 0.8) * 40 + 60) .cornerRadius(20) - .foregroundColor(violet500.opacity(0.8)) + .foregroundColor(.violet500.opacity(0.8)) } .onAppear { self.startAnimation() diff --git a/iosapp/lib-ios/Sources/App/Views/PostInstall.swift b/iosapp/lib-ios/Sources/LibIOS/Views/PostInstall.swift similarity index 100% rename from iosapp/lib-ios/Sources/App/Views/PostInstall.swift rename to iosapp/lib-ios/Sources/LibIOS/Views/PostInstall.swift diff --git a/iosapp/lib-ios/Sources/App/Views/PreReqs.swift b/iosapp/lib-ios/Sources/LibIOS/Views/PreReqs.swift similarity index 96% rename from iosapp/lib-ios/Sources/App/Views/PreReqs.swift rename to iosapp/lib-ios/Sources/LibIOS/Views/PreReqs.swift index d9ea380a..3180fff8 100644 --- a/iosapp/lib-ios/Sources/App/Views/PreReqs.swift +++ b/iosapp/lib-ios/Sources/LibIOS/Views/PreReqs.swift @@ -23,7 +23,7 @@ struct PreReqs: View { HStack { Image(systemName: "checkmark.circle") .font(.system(size: 12, weight: .semibold)) - .foregroundColor(violet500) + .foregroundColor(.violet500) Text(requirement) .font(.footnote) } diff --git a/iosapp/lib-ios/Sources/App/Views/PrimaryButton.swift b/iosapp/lib-ios/Sources/LibIOS/Views/PrimaryButton.swift similarity index 80% rename from iosapp/lib-ios/Sources/App/Views/PrimaryButton.swift rename to iosapp/lib-ios/Sources/LibIOS/Views/PrimaryButton.swift index d6ca65fc..a8a1b0bd 100644 --- a/iosapp/lib-ios/Sources/App/Views/PrimaryButton.swift +++ b/iosapp/lib-ios/Sources/LibIOS/Views/PrimaryButton.swift @@ -8,13 +8,15 @@ struct PrimaryButton: View { var body: some View { Button { - let impact = UIImpactFeedbackGenerator(style: .medium) - impact.impactOccurred() + #if os(iOS) + let impact = UIImpactFeedbackGenerator(style: .medium) + impact.impactOccurred() + #endif } label: { ZStack { VStack { Rectangle() - .fill(Gradient(colors: [violet400, violet500])) + .fill(Gradient(colors: [.violet400, .violet500])) Spacer() Spacer() HStack { @@ -23,9 +25,9 @@ struct PrimaryButton: View { Spacer() Spacer() Rectangle() - .fill(Gradient(colors: [violet500, violet700])) + .fill(Gradient(colors: [.violet500, .violet700])) } - .background(violet500) + .background(Color.violet500) .cornerRadius(20) VStack { Spacer() @@ -39,7 +41,7 @@ struct PrimaryButton: View { } Spacer() } - .background(Gradient(colors: [violet500, violet600])) + .background(Gradient(colors: [.violet500, .violet600])) .foregroundColor(.white) .cornerRadius(20) .padding(.vertical, 2.5) diff --git a/iosapp/lib-ios/Sources/App/Views/Running.swift b/iosapp/lib-ios/Sources/LibIOS/Views/Running.swift similarity index 68% rename from iosapp/lib-ios/Sources/App/Views/Running.swift rename to iosapp/lib-ios/Sources/LibIOS/Views/Running.swift index bae6be17..726f4d40 100644 --- a/iosapp/lib-ios/Sources/App/Views/Running.swift +++ b/iosapp/lib-ios/Sources/LibIOS/Views/Running.swift @@ -2,13 +2,17 @@ import SwiftUI struct Running: View { var body: some View { - VStack(spacing: 20) { + VStack(spacing: 24) { + Image("GertrudeIcon") + .clipShape(RoundedRectangle(cornerRadius: 20)) + .shadow(color: Color(.sRGBLinear, white: 0, opacity: 0.12), radius: 4) + .padding(.bottom, 12) + Text("Gertrude is blocking GIFs and image searches.") - .font(.system(size: 20, weight: .semibold)) + .font(.system(size: 24, weight: .semibold)) .multilineTextAlignment(.center) Text("You can quit this app now—it will keep blocking even when not running.") - .font(.footnote) .multilineTextAlignment(.center) Spacer() diff --git a/iosapp/lib-ios/Sources/App/Views/Welcome.swift b/iosapp/lib-ios/Sources/LibIOS/Views/Welcome.swift similarity index 100% rename from iosapp/lib-ios/Sources/App/Views/Welcome.swift rename to iosapp/lib-ios/Sources/LibIOS/Views/Welcome.swift diff --git a/iosapp/lib-ios/Tests/AppTests/AppTests.swift b/iosapp/lib-ios/Tests/AppTests/AppTests.swift deleted file mode 100644 index ab2d035a..00000000 --- a/iosapp/lib-ios/Tests/AppTests/AppTests.swift +++ /dev/null @@ -1,8 +0,0 @@ -import App -import XCTest - -final class AppTests: XCTestCase { - func testPlaceholder() throws { - XCTAssertTrue(true) - } -} diff --git a/iosapp/lib-ios/Tests/FilterTests/FilterTests.swift b/iosapp/lib-ios/Tests/LibFilterTests/FilterTests.swift similarity index 69% rename from iosapp/lib-ios/Tests/FilterTests/FilterTests.swift rename to iosapp/lib-ios/Tests/LibFilterTests/FilterTests.swift index 47af8f0d..b2ead413 100644 --- a/iosapp/lib-ios/Tests/FilterTests/FilterTests.swift +++ b/iosapp/lib-ios/Tests/LibFilterTests/FilterTests.swift @@ -1,4 +1,4 @@ -import Filter +import LibFilter import XCTest final class FilterTests: XCTestCase { @@ -14,6 +14,11 @@ final class FilterTests: XCTestCase { (host: "giphy.com", url: nil, src: "com.widget", allow: false), (host: "media0.giphy.com", url: nil, src: "com.widget", allow: false), (host: "media.fosu2-1.fna.whatsapp.net", url: nil, src: "", allow: false), + // block these only from Messages app, they allow searching/viewing app store content + (host: "amp-api-edge.apps.apple.com", url: nil, src: ".com.apple.MobileSMS", allow: false), + (host: "amp-api-edge.apps.apple.com", url: nil, src: "com.widget", allow: true), + (host: "is1-ssl.mzstatic.com", url: nil, src: ".com.apple.MobileSMS", allow: false), + (host: "is1-ssl.mzstatic.com", url: nil, src: "com.widget", allow: true), ] for (host, url, src, expected) in cases { diff --git a/iosapp/lib-ios/Tests/LibIOSTests/AppTests.swift b/iosapp/lib-ios/Tests/LibIOSTests/AppTests.swift new file mode 100644 index 00000000..65e74580 --- /dev/null +++ b/iosapp/lib-ios/Tests/LibIOSTests/AppTests.swift @@ -0,0 +1,57 @@ +import ComposableArchitecture +import XCTest + +@testable import LibIOS + +final class AppTests: XCTestCase { + func testAppSendsFirstLaunchEventWhenNoLaunchDatePresent() async throws { + let logDetails = LockIsolated<[String]>([]) + let storedDates = LockIsolated<[Date]>([]) + let store = await TestStore(initialState: AppReducer.State()) { + AppReducer() + } withDependencies: { + $0.date = .constant(.reference) + $0.api.logEvent = { @Sendable id, detail in + logDetails.withValue { $0.append(detail ?? "") } + } + $0.storage.object = { @Sendable key in nil } + $0.storage.set = { @Sendable value, key in + storedDates.withValue { $0.append(value as! Date) } + } + } + + await store.send(.appLaunched) + + await store.receive(.setRunning(false)) { + $0.appState = .welcome + } + await store.receive(.setFirstLaunch(.reference)) { + $0.firstLaunch = .reference + } + + XCTAssertEqual(storedDates.value, [.reference]) + XCTAssertEqual(logDetails.value, ["first launch"]) + } + + func testNoApiEventWhenFirstLaunchPresent() async throws { + let store = await TestStore(initialState: AppReducer.State()) { + AppReducer() + } withDependencies: { + $0.storage.object = { @Sendable _ in Date.epoch } + } + + await store.send(.appLaunched) + + await store.receive(.setRunning(false)) { + $0.appState = .welcome + } + await store.receive(.setFirstLaunch(.epoch)) { + $0.firstLaunch = .epoch + } + } +} + +public extension Date { + static let epoch = Date(timeIntervalSince1970: 0) + static let reference = Date(timeIntervalSinceReferenceDate: 0) +} diff --git a/macapp/App/Sources/LiveApiClient/ApiRequest.swift b/macapp/App/Sources/LiveApiClient/ApiRequest.swift index 01696754..c0f77c00 100644 --- a/macapp/App/Sources/LiveApiClient/ApiRequest.swift +++ b/macapp/App/Sources/LiveApiClient/ApiRequest.swift @@ -48,7 +48,7 @@ private func request( private func data(for request: URLRequest) async throws -> (Data, URLResponse) { return try await withCheckedThrowingContinuation { continuation in URLSession.shared.dataTask(with: request) { data, response, err in - if let err = err { + if let err { continuation.resume(throwing: err) return } diff --git a/pairql-iosapp/Package.resolved b/pairql-iosapp/Package.resolved new file mode 100644 index 00000000..32d3d315 --- /dev/null +++ b/pairql-iosapp/Package.resolved @@ -0,0 +1,51 @@ +{ + "originHash" : "fb4d4e1c156d45d90e7177ad4bf164f1e6c8876ef1a802f1937b89e3923fecb1", + "pins" : [ + { + "identity" : "swift-case-paths", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-case-paths", + "state" : { + "revision" : "5da6989aae464f324eef5c5b52bdb7974725ab81", + "version" : "1.0.0" + } + }, + { + "identity" : "swift-collections", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-collections", + "state" : { + "revision" : "9bf03ff58ce34478e66aaee630e491823326fd06", + "version" : "1.1.3" + } + }, + { + "identity" : "swift-parsing", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-parsing", + "state" : { + "revision" : "a0e7d73f462c1c38c59dc40a3969ac40cea42950", + "version" : "0.13.0" + } + }, + { + "identity" : "swift-url-routing", + "kind" : "remoteSourceControl", + "location" : "https://github.com/gertrude-app/swift-url-routing", + "state" : { + "branch" : "1cf1ca6", + "revision" : "1cf1ca67f4a4e442a599473e320049a85cd31588" + } + }, + { + "identity" : "xctest-dynamic-overlay", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay", + "state" : { + "revision" : "96beb108a57f24c8476ae1f309239270772b2940", + "version" : "1.2.5" + } + } + ], + "version" : 3 +} diff --git a/pairql-iosapp/Package.swift b/pairql-iosapp/Package.swift new file mode 100644 index 00000000..ec43e998 --- /dev/null +++ b/pairql-iosapp/Package.swift @@ -0,0 +1,29 @@ +// swift-tools-version:5.10 +import PackageDescription + +let package = Package( + name: "IOSRoute", + platforms: [.macOS(.v10_15), .iOS(.v17)], + products: [ + .library(name: "IOSRoute", targets: ["IOSRoute"]), + ], + dependencies: [ + // fork avoids swift-syntax transitive dep via case-paths + .package(url: "https://github.com/gertrude-app/swift-url-routing", revision: "1cf1ca6"), + .package(path: "../pairql"), + ], + targets: [ + .target( + name: "IOSRoute", + dependencies: [ + .product(name: "URLRouting", package: "swift-url-routing"), + .product(name: "PairQL", package: "pairql"), + ], + swiftSettings: [.unsafeFlags([ + "-Xfrontend", "-warn-concurrency", + "-Xfrontend", "-enable-actor-data-race-checks", + "-Xfrontend", "-warnings-as-errors", + ])] + ), + ] +) diff --git a/pairql-iosapp/Sources/IOSRoute/IOSRoute.swift b/pairql-iosapp/Sources/IOSRoute/IOSRoute.swift new file mode 100644 index 00000000..4cb16ef3 --- /dev/null +++ b/pairql-iosapp/Sources/IOSRoute/IOSRoute.swift @@ -0,0 +1,15 @@ +import PairQL + +public enum IOSRoute: PairRoute { + case logIOSEvent(LogIOSEvent.Input) +} + +public extension IOSRoute { + nonisolated(unsafe) static let router: AnyParserPrinter = OneOf { + Route(.case(Self.logIOSEvent)) { + Operation(LogIOSEvent.self) + Body(.json(LogIOSEvent.Input.self)) + } + } + .eraseToAnyParserPrinter() +} diff --git a/pairql-iosapp/Sources/IOSRoute/LogIOSEvent.swift b/pairql-iosapp/Sources/IOSRoute/LogIOSEvent.swift new file mode 100644 index 00000000..36796694 --- /dev/null +++ b/pairql-iosapp/Sources/IOSRoute/LogIOSEvent.swift @@ -0,0 +1,33 @@ +import Foundation +import PairQL + +public struct LogIOSEvent: Pair { + public static let auth: ClientAuth = .none + + public struct Input: PairInput { + public var eventId: String + public var kind: String + public var deviceType: String // "iPhone" | "iPad" + public var iOSVersion: String // "18.0.1" + public var vendorId: UUID? + public var detail: String? + + public init( + eventId: String, + kind: String, + deviceType: String, + iOSVersion: String, + vendorId: UUID? = nil, + detail: String? = nil + ) { + self.eventId = eventId + self.kind = kind + self.deviceType = deviceType + self.iOSVersion = iOSVersion + self.vendorId = vendorId + self.detail = detail + } + } + + public typealias Output = Infallible +} diff --git a/pairql-iosapp/project.json b/pairql-iosapp/project.json new file mode 100644 index 00000000..69327bbc --- /dev/null +++ b/pairql-iosapp/project.json @@ -0,0 +1,10 @@ +{ + "root": "pairql-iosapp", + "projectType": "library", + "implicitDependencies": ["pairql"], + "targets": { + "build": { + "command": "cd pairql-iosapp && swift build" + } + } +} diff --git a/pairql-macapp/Tests/MacAppRouteTests/RouterTests.swift b/pairql-macapp/Tests/MacAppRouteTests/RouterTests.swift index 4db2f9ab..45ca7af1 100644 --- a/pairql-macapp/Tests/MacAppRouteTests/RouterTests.swift +++ b/pairql-macapp/Tests/MacAppRouteTests/RouterTests.swift @@ -22,7 +22,7 @@ final class RouterTests: XCTestCase { request.setValue("application/json", forHTTPHeaderField: "Content-Type") request.httpBody = try JSONEncoder().encode(input) - let matched = try router.match(request: request) + let matched = try self.router.match(request: request) let expected = MacAppRoute.userAuthed(self.token, .createSignedScreenshotUpload(input)) expect(matched).toEqual(expected) } @@ -35,7 +35,7 @@ final class RouterTests: XCTestCase { expect(missingHeader).toEqual(nil) request.addValue(self.token.uuidString, forHTTPHeaderField: "X-UserToken") - let matched = try router.match(request: request) + let matched = try self.router.match(request: request) expect(matched).toEqual(route) } } diff --git a/pairql/Package.swift b/pairql/Package.swift index d3ac2d5d..82f6df17 100644 --- a/pairql/Package.swift +++ b/pairql/Package.swift @@ -1,9 +1,9 @@ -// swift-tools-version:5.7 +// swift-tools-version:5.10 import PackageDescription let package = Package( name: "PairQL", - platforms: [.macOS(.v10_15)], + platforms: [.macOS(.v10_15), .iOS(.v17)], products: [ .library(name: "PairQL", targets: ["PairQL"]), ], diff --git a/pairql/Sources/PairQL/Types.swift b/pairql/Sources/PairQL/Types.swift index c3ea3ed3..0fc20ba0 100644 --- a/pairql/Sources/PairQL/Types.swift +++ b/pairql/Sources/PairQL/Types.swift @@ -81,6 +81,11 @@ public struct SuccessOutput: PairOutput { public static var failure: Self { .init(false) } } +public struct Infallible: PairOutput { + private init() {} + public static var success: Self { .init() } +} + extension String: PairOutput {} extension String: PairInput {} extension UUID: PairInput {} diff --git a/x-expect/Package.swift b/x-expect/Package.swift index 0600dbf9..7b720902 100644 --- a/x-expect/Package.swift +++ b/x-expect/Package.swift @@ -1,9 +1,9 @@ -// swift-tools-version:5.7 +// swift-tools-version:5.10 import PackageDescription let package = Package( name: "XExpect", - platforms: [.macOS(.v10_15)], + platforms: [.macOS(.v10_15), .iOS(.v17)], products: [ .library(name: "XExpect", targets: ["XExpect"]), ], diff --git a/x-kit/Package.swift b/x-kit/Package.swift index ad148e79..6fb50835 100644 --- a/x-kit/Package.swift +++ b/x-kit/Package.swift @@ -1,9 +1,9 @@ -// swift-tools-version:5.7 +// swift-tools-version:5.10 import PackageDescription let package = Package( name: "XKit", - platforms: [.macOS(.v10_15)], + platforms: [.macOS(.v10_15), .iOS(.v17)], products: [ .library(name: "XCore", targets: ["XCore"]), .library(name: "XBase64", targets: ["XBase64"]),