diff --git a/TemplateApplication.xcodeproj/project.pbxproj b/TemplateApplication.xcodeproj/project.pbxproj index 1311401..b95f227 100644 --- a/TemplateApplication.xcodeproj/project.pbxproj +++ b/TemplateApplication.xcodeproj/project.pbxproj @@ -20,7 +20,6 @@ 2FA0BFED2ACC977500E0EF83 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 2FA0BFEC2ACC977500E0EF83 /* Localizable.xcstrings */; }; 2FB099AF2A875DF100B20952 /* FirebaseAuth in Frameworks */ = {isa = PBXBuildFile; productRef = 2FB099AE2A875DF100B20952 /* FirebaseAuth */; }; 2FB099B12A875DF100B20952 /* FirebaseFirestore in Frameworks */ = {isa = PBXBuildFile; productRef = 2FB099B02A875DF100B20952 /* FirebaseFirestore */; }; - 2FB099B32A875DF100B20952 /* FirebaseFirestoreSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 2FB099B22A875DF100B20952 /* FirebaseFirestoreSwift */; }; 2FB099B62A875E2B00B20952 /* HealthKitOnFHIR in Frameworks */ = {isa = PBXBuildFile; productRef = 2FB099B52A875E2B00B20952 /* HealthKitOnFHIR */; }; 2FBD738C2A3BD150004228E7 /* SpeziScheduler in Frameworks */ = {isa = PBXBuildFile; productRef = 2FBD738B2A3BD150004228E7 /* SpeziScheduler */; }; 2FC3439029EE6346002D773C /* SocialSupportQuestionnaire.json in Resources */ = {isa = PBXBuildFile; fileRef = 2FE5DC5529EDD811004B9AB4 /* SocialSupportQuestionnaire.json */; }; @@ -67,6 +66,7 @@ 97D73D6A2AD860AD00B47FA0 /* SpeziFirebaseStorage in Frameworks */ = {isa = PBXBuildFile; productRef = 97D73D692AD860AD00B47FA0 /* SpeziFirebaseStorage */; }; A92E4DF02BAA001100AC8DE8 /* OrderedCollections in Frameworks */ = {isa = PBXBuildFile; productRef = A92E4DEF2BAA001100AC8DE8 /* OrderedCollections */; }; A9720E432ABB68CC00872D23 /* AccountSetupHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9720E422ABB68CC00872D23 /* AccountSetupHeader.swift */; }; + A9A3DCC82C75CBBD00FC9B69 /* FirebaseConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9A3DCC72C75CB9A00FC9B69 /* FirebaseConfiguration.swift */; }; A9D83F962B083794000D0C78 /* SpeziFirebaseAccountStorage in Frameworks */ = {isa = PBXBuildFile; productRef = A9D83F952B083794000D0C78 /* SpeziFirebaseAccountStorage */; }; A9DFE8A92ABE551400428242 /* AccountButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9DFE8A82ABE551400428242 /* AccountButton.swift */; }; A9FE7AD02AA39BAB0077B045 /* AccountSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9FE7ACF2AA39BAB0077B045 /* AccountSheet.swift */; }; @@ -131,6 +131,7 @@ 653A256B28338800005D4D48 /* SchedulerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SchedulerTests.swift; sourceTree = ""; }; 653A258928339462005D4D48 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; A9720E422ABB68CC00872D23 /* AccountSetupHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountSetupHeader.swift; sourceTree = ""; }; + A9A3DCC72C75CB9A00FC9B69 /* FirebaseConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirebaseConfiguration.swift; sourceTree = ""; }; A9DFE8A82ABE551400428242 /* AccountButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountButton.swift; sourceTree = ""; }; A9FE7ACF2AA39BAB0077B045 /* AccountSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountSheet.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -146,7 +147,6 @@ 97D73D6A2AD860AD00B47FA0 /* SpeziFirebaseStorage in Frameworks */, 2FE5DC6729EDD894004B9AB4 /* SpeziContact in Frameworks */, 2FE5DC8429EDD934004B9AB4 /* SpeziQuestionnaire in Frameworks */, - 2FB099B32A875DF100B20952 /* FirebaseFirestoreSwift in Frameworks */, 2FB099B12A875DF100B20952 /* FirebaseFirestore in Frameworks */, A9D83F962B083794000D0C78 /* SpeziFirebaseAccountStorage in Frameworks */, 2FB099B62A875E2B00B20952 /* HealthKitOnFHIR in Frameworks */, @@ -286,6 +286,7 @@ 2F4E23822989D51F0013F3D9 /* TemplateApplicationTestingSetup.swift */, A9720E412ABB68B300872D23 /* Account */, 2FE5DC2729EDD38D004B9AB4 /* Contacts */, + A9A3DCC62C75CB8D00FC9B69 /* Firestore */, 2FE5DC2829EDD398004B9AB4 /* Onboarding */, 2FE5DC2D29EDD792004B9AB4 /* Resources */, 2FE5DC3B29EDD7D0004B9AB4 /* Schedule */, @@ -331,6 +332,14 @@ path = Account; sourceTree = ""; }; + A9A3DCC62C75CB8D00FC9B69 /* Firestore */ = { + isa = PBXGroup; + children = ( + A9A3DCC72C75CB9A00FC9B69 /* FirebaseConfiguration.swift */, + ); + path = Firestore; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -345,7 +354,7 @@ buildRules = ( ); dependencies = ( - 2F66D20F2BB723200010D555 /* PBXTargetDependency */, + A9E1D3432C67A3F800CED217 /* PBXTargetDependency */, 56E7083D2BB06FCA00B08F0A /* PBXTargetDependency */, ); name = TemplateApplication; @@ -366,7 +375,6 @@ 2FE5DC8029EDD91D004B9AB4 /* SpeziOnboarding */, 2FB099AE2A875DF100B20952 /* FirebaseAuth */, 2FB099B02A875DF100B20952 /* FirebaseFirestore */, - 2FB099B22A875DF100B20952 /* FirebaseFirestoreSwift */, 2FB099B52A875E2B00B20952 /* HealthKitOnFHIR */, 9739A0C52AD7B5730084BEA5 /* FirebaseStorage */, 97D73D692AD860AD00B47FA0 /* SpeziFirebaseStorage */, @@ -390,6 +398,7 @@ buildRules = ( ); dependencies = ( + A9E1D3462C67B0A300CED217 /* PBXTargetDependency */, 653A255F28338800005D4D48 /* PBXTargetDependency */, ); name = TemplateApplicationTests; @@ -408,6 +417,7 @@ buildRules = ( ); dependencies = ( + A9E1D3482C67B0A700CED217 /* PBXTargetDependency */, 653A256928338800005D4D48 /* PBXTargetDependency */, ); name = TemplateApplicationUITests; @@ -525,6 +535,7 @@ 2FC975A82978F11A00BA99FE /* Home.swift in Sources */, 2FE5DC4E29EDD7FA004B9AB4 /* ScheduleView.swift in Sources */, A9DFE8A92ABE551400428242 /* AccountButton.swift in Sources */, + A9A3DCC82C75CBBD00FC9B69 /* FirebaseConfiguration.swift in Sources */, 2FE5DC3729EDD7CA004B9AB4 /* OnboardingFlow.swift in Sources */, 2F1AC9DF2B4E840E00C24973 /* TemplateApplication.docc in Sources */, 2FF53D8D2A8729D600042B76 /* TemplateApplicationStandard.swift in Sources */, @@ -568,10 +579,6 @@ /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ - 2F66D20F2BB723200010D555 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - productRef = 2F66D20E2BB723200010D555 /* SwiftLintPlugin */; - }; 56E7083D2BB06FCA00B08F0A /* PBXTargetDependency */ = { isa = PBXTargetDependency; productRef = 56E7083C2BB06FCA00B08F0A /* SwiftPackageListPlugin */; @@ -586,6 +593,18 @@ target = 653A254C283387FE005D4D48 /* TemplateApplication */; targetProxy = 653A256828338800005D4D48 /* PBXContainerItemProxy */; }; + A9E1D3432C67A3F800CED217 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + productRef = A9E1D3422C67A3F800CED217 /* SwiftLintBuildToolPlugin */; + }; + A9E1D3462C67B0A300CED217 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + productRef = A9E1D3452C67B0A300CED217 /* SwiftLintBuildToolPlugin */; + }; + A9E1D3482C67B0A700CED217 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + productRef = A9E1D3472C67B0A700CED217 /* SwiftLintBuildToolPlugin */; + }; /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ @@ -649,6 +668,7 @@ SDKROOT = iphoneos; SWIFT_ACTIVE_COMPILATION_CONDITIONS = TEST; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_STRICT_CONCURRENCY = complete; SWIFT_VERSION = 5.0; }; name = Test; @@ -792,6 +812,7 @@ SDKROOT = iphoneos; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_STRICT_CONCURRENCY = complete; SWIFT_VERSION = 5.0; }; name = Debug; @@ -849,6 +870,7 @@ SDKROOT = iphoneos; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; + SWIFT_STRICT_CONCURRENCY = complete; SWIFT_VERSION = 5.0; VALIDATE_PRODUCT = YES; }; @@ -1066,7 +1088,7 @@ repositoryURL = "https://github.com/StanfordSpezi/SpeziScheduler.git"; requirement = { kind = upToNextMinorVersion; - minimumVersion = 0.8.2; + minimumVersion = 0.8.3; }; }; 2F49B7742980407B00BCB272 /* XCRemoteSwiftPackageReference "Spezi" */ = { @@ -1074,7 +1096,7 @@ repositoryURL = "https://github.com/StanfordSpezi/Spezi"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 1.4.0; + minimumVersion = 1.7.1; }; }; 2F66D20D2BB723180010D555 /* XCRemoteSwiftPackageReference "SwiftLint" */ = { @@ -1098,7 +1120,7 @@ repositoryURL = "https://github.com/StanfordSpezi/SpeziAccount.git"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 1.2.0; + minimumVersion = "2.0.0-beta.4"; }; }; 2FE5DC6529EDD894004B9AB4 /* XCRemoteSwiftPackageReference "SpeziContact" */ = { @@ -1106,7 +1128,7 @@ repositoryURL = "https://github.com/StanfordSpezi/SpeziContact.git"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 1.0.0; + minimumVersion = 1.0.2; }; }; 2FE5DC7029EDD8D3004B9AB4 /* XCRemoteSwiftPackageReference "SpeziHealthKit" */ = { @@ -1114,7 +1136,7 @@ repositoryURL = "https://github.com/StanfordSpezi/SpeziHealthKit.git"; requirement = { kind = upToNextMinorVersion; - minimumVersion = 0.5.0; + minimumVersion = 0.6.0; }; }; 2FE5DC7329EDD8E6004B9AB4 /* XCRemoteSwiftPackageReference "SpeziFirebase" */ = { @@ -1122,7 +1144,7 @@ repositoryURL = "https://github.com/StanfordSpezi/SpeziFirebase.git"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 1.1.0; + minimumVersion = "2.0.0-beta.1"; }; }; 2FE5DC8229EDD934004B9AB4 /* XCRemoteSwiftPackageReference "SpeziQuestionnaire" */ = { @@ -1138,7 +1160,7 @@ repositoryURL = "https://github.com/StanfordSpezi/SpeziStorage.git"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 1.0.0; + minimumVersion = 1.1.2; }; }; 2FE5DC8D29EDD980004B9AB4 /* XCRemoteSwiftPackageReference "SpeziViews" */ = { @@ -1146,7 +1168,7 @@ repositoryURL = "https://github.com/StanfordSpezi/SpeziViews.git"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 1.5.0; + minimumVersion = 1.6.0; }; }; 2FE5DC9029EDD9C3004B9AB4 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */ = { @@ -1154,15 +1176,15 @@ repositoryURL = "https://github.com/firebase/firebase-ios-sdk.git"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 10.17.0; + minimumVersion = 11.0.0; }; }; 2FE5DC9729EDD9D9004B9AB4 /* XCRemoteSwiftPackageReference "XCTestExtensions" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/StanfordBDHG/XCTestExtensions.git"; requirement = { - kind = upToNextMinorVersion; - minimumVersion = 0.4.11; + kind = upToNextMajorVersion; + minimumVersion = 1.0.0; }; }; 2FE5DC9A29EDD9EF004B9AB4 /* XCRemoteSwiftPackageReference "XCTHealthKit" */ = { @@ -1186,7 +1208,7 @@ repositoryURL = "https://github.com/StanfordSpezi/SpeziLicense"; requirement = { kind = upToNextMinorVersion; - minimumVersion = 0.1.0; + minimumVersion = 0.1.1; }; }; 97F466E62A76BBEE005DC9B4 /* XCRemoteSwiftPackageReference "SpeziOnboarding" */ = { @@ -1194,7 +1216,7 @@ repositoryURL = "https://github.com/StanfordSpezi/SpeziOnboarding"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 1.2.0; + minimumVersion = 1.2.1; }; }; A92E4DEE2BAA001100AC8DE8 /* XCRemoteSwiftPackageReference "swift-collections" */ = { @@ -1218,11 +1240,6 @@ package = 2F49B7742980407B00BCB272 /* XCRemoteSwiftPackageReference "Spezi" */; productName = Spezi; }; - 2F66D20E2BB723200010D555 /* SwiftLintPlugin */ = { - isa = XCSwiftPackageProductDependency; - package = 2F66D20D2BB723180010D555 /* XCRemoteSwiftPackageReference "SwiftLint" */; - productName = "plugin:SwiftLintPlugin"; - }; 2FB099AE2A875DF100B20952 /* FirebaseAuth */ = { isa = XCSwiftPackageProductDependency; package = 2FE5DC9029EDD9C3004B9AB4 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */; @@ -1233,11 +1250,6 @@ package = 2FE5DC9029EDD9C3004B9AB4 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */; productName = FirebaseFirestore; }; - 2FB099B22A875DF100B20952 /* FirebaseFirestoreSwift */ = { - isa = XCSwiftPackageProductDependency; - package = 2FE5DC9029EDD9C3004B9AB4 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */; - productName = FirebaseFirestoreSwift; - }; 2FB099B52A875E2B00B20952 /* HealthKitOnFHIR */ = { isa = XCSwiftPackageProductDependency; package = 2FB099B42A875E2B00B20952 /* XCRemoteSwiftPackageReference "HealthKitOnFHIR" */; @@ -1347,6 +1359,21 @@ package = 2FE5DC7329EDD8E6004B9AB4 /* XCRemoteSwiftPackageReference "SpeziFirebase" */; productName = SpeziFirebaseAccountStorage; }; + A9E1D3422C67A3F800CED217 /* SwiftLintBuildToolPlugin */ = { + isa = XCSwiftPackageProductDependency; + package = 2F66D20D2BB723180010D555 /* XCRemoteSwiftPackageReference "SwiftLint" */; + productName = "plugin:SwiftLintBuildToolPlugin"; + }; + A9E1D3452C67B0A300CED217 /* SwiftLintBuildToolPlugin */ = { + isa = XCSwiftPackageProductDependency; + package = 2F66D20D2BB723180010D555 /* XCRemoteSwiftPackageReference "SwiftLint" */; + productName = "plugin:SwiftLintBuildToolPlugin"; + }; + A9E1D3472C67B0A700CED217 /* SwiftLintBuildToolPlugin */ = { + isa = XCSwiftPackageProductDependency; + package = 2F66D20D2BB723180010D555 /* XCRemoteSwiftPackageReference "SwiftLint" */; + productName = "plugin:SwiftLintBuildToolPlugin"; + }; /* End XCSwiftPackageProductDependency section */ }; rootObject = 653A2545283387FE005D4D48 /* Project object */; diff --git a/TemplateApplication.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/TemplateApplication.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 9d4ae71..493a021 100644 --- a/TemplateApplication.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/TemplateApplication.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -6,8 +6,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/google/abseil-cpp-binary.git", "state" : { - "revision" : "748c7837511d0e6a507737353af268484e1745e2", - "version" : "1.2024011601.1" + "revision" : "194a6706acbd25e4ef639bcaddea16e8758a3e27", + "version" : "1.2024011602.0" } }, { @@ -15,8 +15,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/antlr/antlr4", "state" : { - "revision" : "7ed420ff2c78d62883875c442d75f32e73bc86c8", - "version" : "4.13.1" + "revision" : "cc82115a4e7f53d71d9d905caa2c2dfa4da58899", + "version" : "4.13.2" } }, { @@ -24,8 +24,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/google/app-check.git", "state" : { - "revision" : "3b62f154d00019ae29a71e9738800bb6f18b236d", - "version" : "10.19.2" + "revision" : "21fe1af9be463a359aaf8d96789ef73fc3760d09", + "version" : "11.0.1" } }, { @@ -42,8 +42,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/krzyzanowskim/CryptoSwift.git", "state" : { - "revision" : "c9c3df6ab812de32bae61fc0cd1bf6d45170ebf0", - "version" : "1.8.2" + "revision" : "678d442c6f7828def400a70ae15968aef67ef52d", + "version" : "1.8.3" } }, { @@ -60,8 +60,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/firebase/firebase-ios-sdk.git", "state" : { - "revision" : "e57841b296d04370ea23580f908881b0ccab17b9", - "version" : "10.28.1" + "revision" : "a5c253d1b4409eb8aef4346015ba000f9935cb2d", + "version" : "11.0.0" } }, { @@ -69,8 +69,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/google/GoogleAppMeasurement.git", "state" : { - "revision" : "fe727587518729046fc1465625b9afd80b5ab361", - "version" : "10.28.0" + "revision" : "ca30c987b732d130732fb60b071e0b655a85ada7", + "version" : "11.0.0" } }, { @@ -78,8 +78,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/google/GoogleDataTransport.git", "state" : { - "revision" : "a637d318ae7ae246b02d7305121275bc75ed5565", - "version" : "9.4.0" + "revision" : "617af071af9aa1d6a091d59a202910ac482128f9", + "version" : "10.1.0" } }, { @@ -87,8 +87,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/google/GoogleUtilities.git", "state" : { - "revision" : "57a1d307f42df690fdef2637f3e5b776da02aad6", - "version" : "7.13.3" + "revision" : "53156c7ec267db846e6b64c9f4c4e31ba4cf75eb", + "version" : "8.0.2" } }, { @@ -96,8 +96,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/google/grpc-binary.git", "state" : { - "revision" : "e9fad491d0673bdda7063a0341fb6b47a30c5359", - "version" : "1.62.2" + "revision" : "f56d8fc3162de9a498377c7b6cea43431f4f5083", + "version" : "1.65.1" } }, { @@ -114,8 +114,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/StanfordBDHG/HealthKitOnFHIR.git", "state" : { - "revision" : "418929f315f37e6d9c8f30f40030bc65b9cc47c9", - "version" : "0.2.8" + "revision" : "b0cfe35a2263a517b22196b559d2dd1d1e2afcd9", + "version" : "0.2.9" } }, { @@ -169,7 +169,7 @@ "location" : "https://github.com/StanfordBDHG/ResearchKitOnFHIR", "state" : { "revision" : "dcd5f95522ed02a873b03c9638dbf397b99c74e0", - "version" : "1.4.0" + "version" : "2.0.0" } }, { @@ -186,8 +186,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/StanfordSpezi/Spezi", "state" : { - "revision" : "5ada3f3a18c86a658d140c6f4c45ca2e9d5e61ce", - "version" : "1.4.0" + "revision" : "87d735b4db005b1ff825bb3b55733cb78a69f899", + "version" : "1.7.1" } }, { @@ -195,8 +195,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/StanfordSpezi/SpeziAccount.git", "state" : { - "revision" : "2de07209430fe7b13c44790eab948b30482fcb9d", - "version" : "1.2.4" + "revision" : "40fa3f5e84d22c700ad152e1a29f459cc5e41447", + "version" : "2.0.0-beta.4" } }, { @@ -204,8 +204,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/StanfordSpezi/SpeziContact.git", "state" : { - "revision" : "494b776f8c98d771e4a609a1fb706097dba4c030", - "version" : "1.0.0" + "revision" : "8dd7cb426e79f30ced23f37e438c0ca38bfe9a47", + "version" : "1.0.2" } }, { @@ -213,8 +213,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/StanfordSpezi/SpeziFirebase.git", "state" : { - "revision" : "00ff0db12bf72ba39354e263d8f916ae8392368b", - "version" : "1.1.2" + "revision" : "7855a911c7309bf34d3cb693f76989ede15737fc", + "version" : "2.0.0-beta.1" } }, { @@ -222,8 +222,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/StanfordSpezi/SpeziFoundation.git", "state" : { - "revision" : "3326feab3dac120c16af63243615592990d33516", - "version" : "1.1.2" + "revision" : "d3a675735c734001e29e51689217d160bde51ce2", + "version" : "2.0.0-beta.1" } }, { @@ -231,8 +231,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/StanfordSpezi/SpeziHealthKit.git", "state" : { - "revision" : "1e9cb5a6036ac7f4ff37ea1c3ed4898103339ad1", - "version" : "0.5.3" + "revision" : "fbdec78fcb2f90d6338f1968e21dd11fbee65070", + "version" : "0.6.0" } }, { @@ -240,8 +240,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/StanfordSpezi/SpeziLicense", "state" : { - "revision" : "d839e7138ca2e231f5bdd90c3e4ab06dba9bc30b", - "version" : "0.1.0" + "revision" : "2249ce615a624a072834e31e7906b779ba82b824", + "version" : "0.1.1" } }, { @@ -249,8 +249,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/StanfordSpezi/SpeziOnboarding", "state" : { - "revision" : "8d6dda3501720a1952573439b21a503cbecd9e0f", - "version" : "1.2.0" + "revision" : "ec570fe5b26bd80a0ee7bc047ae680767621aaa4", + "version" : "1.2.1" } }, { @@ -258,8 +258,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/StanfordSpezi/SpeziQuestionnaire.git", "state" : { - "revision" : "76732ebcb0b521fa94d41c38a5fe35e6e1a87173", - "version" : "1.2.1" + "revision" : "853585fbdd5d2d0af440c805d629a025ef5545d9", + "version" : "1.2.2" } }, { @@ -267,8 +267,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/StanfordSpezi/SpeziScheduler.git", "state" : { - "revision" : "eed3980f20b01a788720c869010e3fe2fbfcd1fd", - "version" : "0.8.2" + "revision" : "896eb442eb2941f9b2f7721c3ac871373934142a", + "version" : "0.8.3" } }, { @@ -276,8 +276,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/StanfordSpezi/SpeziStorage.git", "state" : { - "revision" : "b958df9b31f24800388a7bfc28f457ce7b82556c", - "version" : "1.0.2" + "revision" : "099e05983eaf5315d3e5ddc67c9c44c96387e403", + "version" : "1.1.2" } }, { @@ -285,8 +285,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/StanfordSpezi/SpeziViews.git", "state" : { - "revision" : "ff61e6594677572df051b96905cc2a7a12cffd10", - "version" : "1.5.0" + "revision" : "427f4f3a7acb0e00ea11c4c3ca7b60e36d2557a0", + "version" : "1.6.0" } }, { @@ -294,8 +294,17 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-argument-parser.git", "state" : { - "revision" : "0fbc8848e389af3bb55c182bc19ca9d5dc2f255b", - "version" : "1.4.0" + "revision" : "41982a3656a71c768319979febd796c6fd111d5c", + "version" : "1.5.0" + } + }, + { + "identity" : "swift-atomics", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-atomics", + "state" : { + "revision" : "cd142fd2f64be2100422d658e7411e39489da985", + "version" : "1.2.0" } }, { @@ -303,8 +312,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-collections.git", "state" : { - "revision" : "ee97538f5b81ae89698fd95938896dec5217b148", - "version" : "1.1.1" + "revision" : "3d2dc41a01f9e49d84f0a3925fb858bed64f702d", + "version" : "1.1.2" } }, { @@ -321,8 +330,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-protobuf.git", "state" : { - "revision" : "9f0c76544701845ad98716f3f6a774a892152bcb", - "version" : "1.26.0" + "revision" : "e17d61f26df0f0e06f58f6977ba05a097a720106", + "version" : "1.27.1" } }, { @@ -330,8 +339,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-syntax.git", "state" : { - "revision" : "303e5c5c36d6a558407d364878df131c3546fad8", - "version" : "510.0.2" + "revision" : "2bc86522d115234d1f588efe2bcb4ce4be8f8b82", + "version" : "510.0.3" } }, { @@ -366,8 +375,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/StanfordBDHG/XCTestExtensions.git", "state" : { - "revision" : "69eae1a4490d3ec4f3cd20584aef3df9186e0904", - "version" : "0.4.11" + "revision" : "aad6c161a09d658f30c7170deb4a61e8916a4a4c", + "version" : "1.0.0" } }, { @@ -384,8 +393,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/StanfordBDHG/XCTRuntimeAssertions", "state" : { - "revision" : "51da3403f128b120705571ce61e0fe190f8889e6", - "version" : "1.0.1" + "revision" : "a2b127559cae78ae497273f047ccd99b28bf319b", + "version" : "1.1.2" } }, { @@ -393,8 +402,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/jpsim/Yams.git", "state" : { - "revision" : "9234124cff5e22e178988c18d8b95a8ae8007f76", - "version" : "5.1.2" + "revision" : "3036ba9d69cf1fd04d433527bc339dc0dc75433d", + "version" : "5.1.3" } } ], diff --git a/TemplateApplication/Account/AccountButton.swift b/TemplateApplication/Account/AccountButton.swift index ca53d00..60c429a 100644 --- a/TemplateApplication/Account/AccountButton.swift +++ b/TemplateApplication/Account/AccountButton.swift @@ -10,18 +10,13 @@ import SwiftUI struct AccountButton: View { - static let shouldDisplay = !FeatureFlags.disableFirebase || ProcessInfo.processInfo.isPreviewSimulator - @Binding private var isPresented: Bool var body: some View { - Button(action: { + Button("ACCOUNT_TITLE", systemImage: "person.crop.circle") { isPresented = true - }) { - Image(systemName: "person.crop.circle") } - .accessibilityLabel("ACCOUNT_TITLE") } diff --git a/TemplateApplication/Account/AccountSetupHeader.swift b/TemplateApplication/Account/AccountSetupHeader.swift index b89cf1c..565aa22 100644 --- a/TemplateApplication/Account/AccountSetupHeader.swift +++ b/TemplateApplication/Account/AccountSetupHeader.swift @@ -6,7 +6,7 @@ // SPDX-License-Identifier: MIT // -import SpeziAccount +@_spi(TestingSupport) import SpeziAccount import SwiftUI @@ -38,6 +38,8 @@ struct AccountSetupHeader: View { #if DEBUG #Preview { AccountSetupHeader() - .environment(Account()) + .previewWith { + AccountConfiguration(service: InMemoryAccountService()) + } } #endif diff --git a/TemplateApplication/Account/AccountSheet.swift b/TemplateApplication/Account/AccountSheet.swift index 7fce619..e720b8e 100644 --- a/TemplateApplication/Account/AccountSheet.swift +++ b/TemplateApplication/Account/AccountSheet.swift @@ -6,7 +6,7 @@ // SPDX-License-Identifier: MIT // -import SpeziAccount +@_spi(TestingSupport) import SpeziAccount import SpeziLicense import SwiftUI @@ -18,28 +18,19 @@ struct AccountSheet: View { @Environment(\.accountRequired) var accountRequired @State var isInSetup = false - @State var overviewIsEditing = false var body: some View { NavigationStack { ZStack { if account.signedIn && !isInSetup { - AccountOverview(isEditing: $overviewIsEditing) { + AccountOverview(close: .showCloseButton) { NavigationLink { ContributionsList(projectLicense: .mit) } label: { Text("LICENSE_INFO_TITLE") } } - .onDisappear { - overviewIsEditing = false - } - .toolbar { - if !overviewIsEditing { - closeButton - } - } } else { AccountSetup { _ in dismiss() // we just signed in, dismiss the account setup sheet @@ -71,22 +62,20 @@ struct AccountSheet: View { #if DEBUG #Preview("AccountSheet") { - let details = AccountDetails.Builder() - .set(\.userId, value: "lelandstanford@stanford.edu") - .set(\.name, value: PersonNameComponents(givenName: "Leland", familyName: "Stanford")) + var details = AccountDetails() + details.userId = "lelandstanford@stanford.edu" + details.name = PersonNameComponents(givenName: "Leland", familyName: "Stanford") return AccountSheet() .previewWith { - AccountConfiguration(building: details, active: MockUserIdPasswordAccountService()) + AccountConfiguration(service: InMemoryAccountService(), activeDetails: details) } } #Preview("AccountSheet SignIn") { AccountSheet() .previewWith { - AccountConfiguration { - MockUserIdPasswordAccountService() - } + AccountConfiguration(service: InMemoryAccountService()) } } #endif diff --git a/TemplateApplication/Contacts/Contacts.swift b/TemplateApplication/Contacts/Contacts.swift index e9b5a75..3ca48f8 100644 --- a/TemplateApplication/Contacts/Contacts.swift +++ b/TemplateApplication/Contacts/Contacts.swift @@ -7,6 +7,7 @@ // import Foundation +import SpeziAccount import SpeziContact import SwiftUI @@ -48,7 +49,9 @@ struct Contacts: View { ] ) ] - + + @Environment(Account.self) private var account: Account? + @Binding var presentingAccount: Bool @@ -57,7 +60,7 @@ struct Contacts: View { ContactsList(contacts: contacts) .navigationTitle(String(localized: "CONTACTS_NAVIGATION_TITLE")) .toolbar { - if AccountButton.shouldDisplay { + if account != nil { AccountButton(isPresented: $presentingAccount) } } diff --git a/TemplateApplication/Firestore/FirebaseConfiguration.swift b/TemplateApplication/Firestore/FirebaseConfiguration.swift new file mode 100644 index 0000000..2f07d29 --- /dev/null +++ b/TemplateApplication/Firestore/FirebaseConfiguration.swift @@ -0,0 +1,94 @@ +// +// This source file is part of the Stanford Spezi Template Application open-source project +// +// SPDX-FileCopyrightText: 2023 Stanford University +// +// SPDX-License-Identifier: MIT +// + +import FirebaseFirestore +import FirebaseStorage +import Spezi +import SpeziAccount +import SpeziFirebaseAccount + + +final class FirebaseConfiguration: Module, DefaultInitializable, @unchecked Sendable { + enum ConfigurationError: Error { + case userNotAuthenticatedYet + } + + static var userCollection: CollectionReference { + Firestore.firestore().collection("users") + } + + + @MainActor var userDocumentReference: DocumentReference { + get throws { + guard let details = account?.details else { + throw ConfigurationError.userNotAuthenticatedYet + } + + return userDocumentReference(for: details.accountId) + } + } + + @MainActor var userBucketReference: StorageReference { + get throws { + guard let details = account?.details else { + throw ConfigurationError.userNotAuthenticatedYet + } + + return Storage.storage().reference().child("users/\(details.accountId)") + } + } + + @Application(\.logger) private var logger + + @Dependency(Account.self) private var account: Account? // optional, as Firebase might be disabled + @Dependency(FirebaseAccountService.self) private var accountService: FirebaseAccountService? + + init() {} + + func userDocumentReference(for accountId: String) -> DocumentReference { + Self.userCollection.document(accountId) + } + + + func configure() { + Task { + await setupTestAccount() + } + } + + + private func setupTestAccount() async { + guard let accountService, FeatureFlags.setupTestAccount else { + return + } + + do { + try await accountService.login(userId: "lelandstanford@stanford.edu", password: "StanfordRocks!") + return + } catch { + guard let accountError = error as? FirebaseAccountError, + case .invalidCredentials = accountError else { + logger.error("Failed to login into test account: \(error)") + return + } + } + + // account doesn't exist yet, signup + var details = AccountDetails() + details.userId = "lelandstanford@stanford.edu" + details.password = "StanfordRocks!" + details.name = PersonNameComponents(givenName: "Leland", familyName: "Stanford") + details.genderIdentity = .male + + do { + try await accountService.signUp(with: details) + } catch { + logger.error("Failed to setup test account: \(error)") + } + } +} diff --git a/TemplateApplication/Home.swift b/TemplateApplication/Home.swift index 4cdd09b..d4fbdc2 100644 --- a/TemplateApplication/Home.swift +++ b/TemplateApplication/Home.swift @@ -6,7 +6,7 @@ // SPDX-License-Identifier: MIT // -import SpeziAccount +@_spi(TestingSupport) import SpeziAccount import SwiftUI @@ -15,10 +15,6 @@ struct HomeView: View { case schedule case contact } - - static var accountEnabled: Bool { - !FeatureFlags.disableFirebase && !FeatureFlags.skipOnboarding - } @AppStorage(StorageKeys.homeTabSelection) private var selectedTab = Tabs.schedule @@ -41,35 +37,23 @@ struct HomeView: View { .sheet(isPresented: $presentingAccount) { AccountSheet() } - .accountRequired(Self.accountEnabled) { + .accountRequired(!FeatureFlags.disableFirebase && !FeatureFlags.skipOnboarding) { AccountSheet() } - .verifyRequiredAccountDetails(Self.accountEnabled) } } #if DEBUG #Preview { - let details = AccountDetails.Builder() - .set(\.userId, value: "lelandstanford@stanford.edu") - .set(\.name, value: PersonNameComponents(givenName: "Leland", familyName: "Stanford")) + var details = AccountDetails() + details.userId = "lelandstanford@stanford.edu" + details.name = PersonNameComponents(givenName: "Leland", familyName: "Stanford") return HomeView() .previewWith(standard: TemplateApplicationStandard()) { TemplateApplicationScheduler() - AccountConfiguration(building: details, active: MockUserIdPasswordAccountService()) - } -} - -#Preview { - CommandLine.arguments.append("--disableFirebase") // make sure the MockWebService is displayed - return HomeView() - .previewWith(standard: TemplateApplicationStandard()) { - TemplateApplicationScheduler() - AccountConfiguration { - MockUserIdPasswordAccountService() - } + AccountConfiguration(service: InMemoryAccountService(), activeDetails: details) } } #endif diff --git a/TemplateApplication/Onboarding/AccountOnboarding.swift b/TemplateApplication/Onboarding/AccountOnboarding.swift index f7d1eaf..815ba28 100644 --- a/TemplateApplication/Onboarding/AccountOnboarding.swift +++ b/TemplateApplication/Onboarding/AccountOnboarding.swift @@ -6,7 +6,7 @@ // SPDX-License-Identifier: MIT // -import SpeziAccount +@_spi(TestingSupport) import SpeziAccount import SpeziOnboarding import SwiftUI @@ -42,22 +42,20 @@ struct AccountOnboarding: View { AccountOnboarding() } .previewWith { - AccountConfiguration { - MockUserIdPasswordAccountService() - } + AccountConfiguration(service: InMemoryAccountService()) } } #Preview("Account Onboarding") { - let details = AccountDetails.Builder() - .set(\.userId, value: "lelandstanford@stanford.edu") - .set(\.name, value: PersonNameComponents(givenName: "Leland", familyName: "Stanford")) + var details = AccountDetails() + details.userId = "lelandstanford@stanford.edu" + details.name = PersonNameComponents(givenName: "Leland", familyName: "Stanford") return OnboardingStack { AccountOnboarding() } .previewWith { - AccountConfiguration(building: details, active: MockUserIdPasswordAccountService()) + AccountConfiguration(service: InMemoryAccountService(), activeDetails: details) } } #endif diff --git a/TemplateApplication/Onboarding/OnboardingFlow.swift b/TemplateApplication/Onboarding/OnboardingFlow.swift index a6ee1f9..d38e53d 100644 --- a/TemplateApplication/Onboarding/OnboardingFlow.swift +++ b/TemplateApplication/Onboarding/OnboardingFlow.swift @@ -6,10 +6,11 @@ // SPDX-License-Identifier: MIT // -import SpeziAccount +@_spi(TestingSupport) import SpeziAccount import SpeziFirebaseAccount import SpeziHealthKit import SpeziOnboarding +import SpeziScheduler import SwiftUI @@ -23,7 +24,7 @@ struct OnboardingFlow: View { @State private var localNotificationAuthorization = false - private var healthKitAuthorization: Bool { + @MainActor private var healthKitAuthorization: Bool { // As HealthKit not available in preview simulator if ProcessInfo.processInfo.isPreviewSimulator { return false @@ -65,13 +66,10 @@ struct OnboardingFlow: View { #if DEBUG #Preview { OnboardingFlow() - .environment(Account(MockUserIdPasswordAccountService())) .previewWith(standard: TemplateApplicationStandard()) { OnboardingDataSource() HealthKit() - AccountConfiguration { - MockUserIdPasswordAccountService() - } + AccountConfiguration(service: InMemoryAccountService()) TemplateApplicationScheduler() } diff --git a/TemplateApplication/Schedule/ScheduleView.swift b/TemplateApplication/Schedule/ScheduleView.swift index 4652fe4..9413cac 100644 --- a/TemplateApplication/Schedule/ScheduleView.swift +++ b/TemplateApplication/Schedule/ScheduleView.swift @@ -7,7 +7,7 @@ // import OrderedCollections -import SpeziAccount +@_spi(TestingSupport) import SpeziAccount import SpeziQuestionnaire import SpeziScheduler import SwiftUI @@ -16,12 +16,13 @@ import SwiftUI struct ScheduleView: View { @Environment(TemplateApplicationStandard.self) private var standard @Environment(TemplateApplicationScheduler.self) private var scheduler + @Environment(Account.self) private var account: Account? @State private var presentedContext: EventContext? @Binding private var presentingAccount: Bool - private var eventContextsByDate: OrderedDictionary { + @MainActor private var eventContextsByDate: OrderedDictionary { let eventContexts = scheduler.tasks.flatMap { task in task .events( @@ -59,7 +60,7 @@ struct ScheduleView: View { destination(withContext: presentedContext) } .toolbar { - if AccountButton.shouldDisplay { + if account != nil { AccountButton(isPresented: $presentingAccount) } } @@ -72,7 +73,8 @@ struct ScheduleView: View { self._presentingAccount = presentingAccount } - + + @MainActor private func destination(withContext eventContext: EventContext) -> some View { @ViewBuilder var destination: some View { switch eventContext.task.context { @@ -89,7 +91,7 @@ struct ScheduleView: View { } case let .test(string): ModalView(text: string, buttonText: String(localized: "CLOSE")) { - await eventContext.event.complete(true) + eventContext.event.complete(true) } } } @@ -111,9 +113,7 @@ struct ScheduleView: View { ScheduleView(presentingAccount: .constant(false)) .previewWith(standard: TemplateApplicationStandard()) { TemplateApplicationScheduler() - AccountConfiguration { - MockUserIdPasswordAccountService() - } + AccountConfiguration(service: InMemoryAccountService()) } } #endif diff --git a/TemplateApplication/Schedule/TemplateApplicationTaskContext.swift b/TemplateApplication/Schedule/TemplateApplicationTaskContext.swift index 58c2356..d7cccb0 100644 --- a/TemplateApplication/Schedule/TemplateApplicationTaskContext.swift +++ b/TemplateApplication/Schedule/TemplateApplicationTaskContext.swift @@ -13,6 +13,7 @@ import ModelsR4 /// The context attached to each task in the Spezi Template Application. /// /// We currently only support `Questionnaire`s, more cases can be added in the future. +@MainActor enum TemplateApplicationTaskContext: Codable, Identifiable { /// The task should display a `Questionnaire`. case questionnaire(Questionnaire) @@ -20,7 +21,7 @@ enum TemplateApplicationTaskContext: Codable, Identifiable { case test(String) - var id: FHIRPrimitive? { + nonisolated var id: FHIRPrimitive? { switch self { case let .questionnaire(questionnaire): return questionnaire.id diff --git a/TemplateApplication/SharedContext/FeatureFlags.swift b/TemplateApplication/SharedContext/FeatureFlags.swift index 29e4e56..8829f6b 100644 --- a/TemplateApplication/SharedContext/FeatureFlags.swift +++ b/TemplateApplication/SharedContext/FeatureFlags.swift @@ -23,4 +23,8 @@ enum FeatureFlags { #endif /// Adds a test task to the schedule at the current time static let testSchedule = CommandLine.arguments.contains("--testSchedule") + /// Automatically sign in into a test account upon app launch. + /// + /// Requires ``disableFirebase`` to be `false`. + static let setupTestAccount = CommandLine.arguments.contains("--setupTestAccount") } diff --git a/TemplateApplication/TemplateApplicationDelegate.swift b/TemplateApplication/TemplateApplicationDelegate.swift index 06c4053..631c5d3 100644 --- a/TemplateApplication/TemplateApplicationDelegate.swift +++ b/TemplateApplication/TemplateApplicationDelegate.swift @@ -6,9 +6,12 @@ // SPDX-License-Identifier: MIT // +import class FirebaseFirestore.FirestoreSettings +import class FirebaseFirestore.MemoryCacheSettings import Spezi import SpeziAccount import SpeziFirebaseAccount +import SpeziFirebaseAccountStorage import SpeziFirebaseStorage import SpeziFirestore import SpeziHealthKit @@ -21,23 +24,19 @@ class TemplateApplicationDelegate: SpeziAppDelegate { override var configuration: Configuration { Configuration(standard: TemplateApplicationStandard()) { if !FeatureFlags.disableFirebase { - AccountConfiguration(configuration: [ - .requires(\.userId), - .requires(\.name), + AccountConfiguration( + service: FirebaseAccountService(providers: [.emailAndPassword, .signInWithApple], emulatorSettings: accountEmulator), + storageProvider: FirestoreAccountStorage(storeIn: FirebaseConfiguration.userCollection), + configuration: [ + .requires(\.userId), + .requires(\.name), - // additional values stored using the `FirestoreAccountStorage` within our Standard implementation - .collects(\.genderIdentity), - .collects(\.dateOfBirth) - ]) + // additional values stored using the `FirestoreAccountStorage` within our Standard implementation + .collects(\.genderIdentity), + .collects(\.dateOfBirth) + ] + ) - if FeatureFlags.useFirebaseEmulator { - FirebaseAccountConfiguration( - authenticationMethods: [.emailAndPassword, .signInWithApple], - emulatorSettings: (host: "localhost", port: 9099) - ) - } else { - FirebaseAccountConfiguration(authenticationMethods: [.emailAndPassword, .signInWithApple]) - } firestore if FeatureFlags.useFirebaseEmulator { FirebaseStorageConfiguration(emulatorSettings: (host: "localhost", port: 9199)) @@ -54,7 +53,15 @@ class TemplateApplicationDelegate: SpeziAppDelegate { OnboardingDataSource() } } - + + private var accountEmulator: (host: String, port: Int)? { + if FeatureFlags.useFirebaseEmulator { + (host: "localhost", port: 9099) + } else { + nil + } + } + private var firestore: Firestore { let settings = FirestoreSettings() diff --git a/TemplateApplication/TemplateApplicationStandard.swift b/TemplateApplication/TemplateApplicationStandard.swift index a28cc47..4262d89 100644 --- a/TemplateApplication/TemplateApplicationStandard.swift +++ b/TemplateApplication/TemplateApplicationStandard.swift @@ -6,14 +6,14 @@ // SPDX-License-Identifier: MIT // -import FirebaseFirestore -import FirebaseStorage +@preconcurrency import FirebaseFirestore +@preconcurrency import FirebaseStorage import HealthKitOnFHIR import OSLog import PDFKit import Spezi import SpeziAccount -import SpeziFirebaseAccountStorage +import SpeziFirebaseAccount import SpeziFirestore import SpeziHealthKit import SpeziOnboarding @@ -25,48 +25,12 @@ actor TemplateApplicationStandard: Standard, EnvironmentAccessible, HealthKitConstraint, OnboardingConstraint, - AccountStorageConstraint, AccountNotifyConstraint { - enum TemplateApplicationStandardError: Error { - case userNotAuthenticatedYet - } - - private static var userCollection: CollectionReference { - Firestore.firestore().collection("users") - } - - @Dependency var accountStorage: FirestoreAccountStorage? - - @AccountReference var account: Account @Application(\.logger) private var logger - - - private var userDocumentReference: DocumentReference { - get async throws { - guard let details = await account.details else { - throw TemplateApplicationStandardError.userNotAuthenticatedYet - } - - return Self.userCollection.document(details.accountId) - } - } - - private var userBucketReference: StorageReference { - get async throws { - guard let details = await account.details else { - throw TemplateApplicationStandardError.userNotAuthenticatedYet - } - - return Storage.storage().reference().child("users/\(details.accountId)") - } - } + @Dependency(FirebaseConfiguration.self) private var configuration - init() { - if !FeatureFlags.disableFirebase { - _accountStorage = Dependency(wrappedValue: FirestoreAccountStorage(storeIn: TemplateApplicationStandard.userCollection)) - } - } + init() {} func add(sample: HKSample) async { @@ -76,7 +40,8 @@ actor TemplateApplicationStandard: Standard, } do { - try await healthKitDocument(id: sample.id).setData(from: sample.resource) + try await healthKitDocument(id: sample.id) + .setData(from: sample.resource) } catch { logger.error("Could not store HealthKit sample: \(error)") } @@ -105,7 +70,7 @@ actor TemplateApplicationStandard: Standard, } do { - try await userDocumentReference + try await configuration.userDocumentReference .collection("QuestionnaireResponse") // Add all HealthKit sources in a /QuestionnaireResponse collection. .document(id) // Set the document identifier to the id of the response. .setData(from: response) @@ -116,31 +81,33 @@ actor TemplateApplicationStandard: Standard, private func healthKitDocument(id uuid: UUID) async throws -> DocumentReference { - try await userDocumentReference + try await configuration.userDocumentReference .collection("HealthKit") // Add all HealthKit sources in a /HealthKit collection. .document(uuid.uuidString) // Set the document identifier to the UUID of the document. } - func deletedAccount() async throws { - // delete all user associated data - do { - try await userDocumentReference.delete() - } catch { - logger.error("Could not delete user document: \(error)") + func respondToEvent(_ event: AccountNotifications.Event) async { + if case let .deletingAccount(accountId) = event { + do { + try await configuration.userDocumentReference(for: accountId).delete() + } catch { + logger.error("Could not delete user document: \(error)") + } } } /// Stores the given consent form in the user's document directory with a unique timestamped filename. /// /// - Parameter consent: The consent form's data to be stored as a `PDFDocument`. + @MainActor func store(consent: PDFDocument) async { let formatter = DateFormatter() formatter.dateFormat = "yyyy-MM-dd_HHmmss" let dateString = formatter.string(from: Date()) - + guard !FeatureFlags.disableFirebase else { guard let basePath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { - logger.error("Could not create path for writing consent form to user document directory.") + await logger.error("Could not create path for writing consent form to user document directory.") return } @@ -152,51 +119,17 @@ actor TemplateApplicationStandard: Standard, do { guard let consentData = consent.dataRepresentation() else { - logger.error("Could not store consent form.") + await logger.error("Could not store consent form.") return } - + let metadata = StorageMetadata() metadata.contentType = "application/pdf" - _ = try await userBucketReference.child("consent/\(dateString).pdf").putDataAsync(consentData, metadata: metadata) + _ = try await configuration.userBucketReference + .child("consent/\(dateString).pdf") + .putDataAsync(consentData, metadata: metadata) { @Sendable _ in } } catch { - logger.error("Could not store consent form: \(error)") - } - } - - - func create(_ identifier: AdditionalRecordId, _ details: SignupDetails) async throws { - guard let accountStorage else { - preconditionFailure("Account Storage was requested although not enabled in current configuration.") - } - try await accountStorage.create(identifier, details) - } - - func load(_ identifier: AdditionalRecordId, _ keys: [any AccountKey.Type]) async throws -> PartialAccountDetails { - guard let accountStorage else { - preconditionFailure("Account Storage was requested although not enabled in current configuration.") - } - return try await accountStorage.load(identifier, keys) - } - - func modify(_ identifier: AdditionalRecordId, _ modifications: AccountModifications) async throws { - guard let accountStorage else { - preconditionFailure("Account Storage was requested although not enabled in current configuration.") - } - try await accountStorage.modify(identifier, modifications) - } - - func clear(_ identifier: AdditionalRecordId) async { - guard let accountStorage else { - preconditionFailure("Account Storage was requested although not enabled in current configuration.") - } - await accountStorage.clear(identifier) - } - - func delete(_ identifier: AdditionalRecordId) async throws { - guard let accountStorage else { - preconditionFailure("Account Storage was requested although not enabled in current configuration.") + await logger.error("Could not store consent form: \(error)") } - try await accountStorage.delete(identifier) } } diff --git a/TemplateApplication/TemplateApplicationTestingSetup.swift b/TemplateApplication/TemplateApplicationTestingSetup.swift index b0b98ee..bac8412 100644 --- a/TemplateApplication/TemplateApplicationTestingSetup.swift +++ b/TemplateApplication/TemplateApplicationTestingSetup.swift @@ -15,7 +15,7 @@ private struct TemplateAppTestingSetup: ViewModifier { func body(content: Content) -> some View { content - .task { + .onAppear { if FeatureFlags.skipOnboarding { completedOnboardingFlow = true } diff --git a/TemplateApplicationTests/TemplateApplicationTests.swift b/TemplateApplicationTests/TemplateApplicationTests.swift index e019d9b..af006a4 100644 --- a/TemplateApplicationTests/TemplateApplicationTests.swift +++ b/TemplateApplicationTests/TemplateApplicationTests.swift @@ -11,6 +11,7 @@ import XCTest class TemplateApplicationTests: XCTestCase { + @MainActor func testContactsCount() throws { XCTAssertEqual(Contacts(presentingAccount: .constant(true)).contacts.count, 1) } diff --git a/TemplateApplicationUITests/ContactsTests.swift b/TemplateApplicationUITests/ContactsTests.swift index 672ebc3..d19adba 100644 --- a/TemplateApplicationUITests/ContactsTests.swift +++ b/TemplateApplicationUITests/ContactsTests.swift @@ -10,9 +10,8 @@ import XCTest class ContactsTests: XCTestCase { - override func setUpWithError() throws { - try super.setUpWithError() - + @MainActor + override func setUp() async throws { continueAfterFailure = false let app = XCUIApplication() @@ -20,20 +19,23 @@ class ContactsTests: XCTestCase { app.launch() } - + + @MainActor func testContacts() throws { let app = XCUIApplication() - - XCTAssertTrue(app.tabBars["Tab Bar"].buttons["Contacts"].waitForExistence(timeout: 2)) + + XCTAssertTrue(app.wait(for: .runningForeground, timeout: 2.0)) + + XCTAssertTrue(app.tabBars["Tab Bar"].buttons["Contacts"].exists) app.tabBars["Tab Bar"].buttons["Contacts"].tap() XCTAssertTrue(app.staticTexts["Contact: Leland Stanford"].waitForExistence(timeout: 2)) - XCTAssertTrue(app.buttons["Call"].waitForExistence(timeout: 2)) - XCTAssertTrue(app.buttons["Text"].waitForExistence(timeout: 2)) - XCTAssertTrue(app.buttons["Email"].waitForExistence(timeout: 2)) - XCTAssertTrue(app.buttons["Website"].waitForExistence(timeout: 2)) + XCTAssertTrue(app.buttons["Call"].exists) + XCTAssertTrue(app.buttons["Text"].exists) + XCTAssertTrue(app.buttons["Email"].exists) + XCTAssertTrue(app.buttons["Website"].exists) - XCTAssertTrue(app.buttons["Address: 450 Serra Mall\nStanford CA 94305\nUSA"].waitForExistence(timeout: 2)) + XCTAssertTrue(app.buttons["Address: 450 Serra Mall\nStanford CA 94305\nUSA"].exists) } } diff --git a/TemplateApplicationUITests/ContributionsTest.swift b/TemplateApplicationUITests/ContributionsTest.swift index 768d6ba..175435d 100644 --- a/TemplateApplicationUITests/ContributionsTest.swift +++ b/TemplateApplicationUITests/ContributionsTest.swift @@ -10,30 +10,28 @@ import XCTest final class ContributionsTest: XCTestCase { - override func setUpWithError() throws { - try super.setUpWithError() - + @MainActor + override func setUp() async throws { continueAfterFailure = false let app = XCUIApplication() - app.launchArguments = ["--showOnboarding"] + app.launchArguments = ["--setupTestAccount", "--skipOnboarding"] app.deleteAndLaunch(withSpringboardAppName: "TemplateApplication") } + @MainActor func testLicenseInformationPage() throws { let app = XCUIApplication() - // complete onboarding so user is logged in - try app.conductOnboardingIfNeeded(email: "leland@contributions.stanford.edu") - + XCTAssertTrue(app.wait(for: .runningForeground, timeout: 2.0)) - XCTAssertTrue(app.buttons["Your Account"].waitForExistence(timeout: 6.0)) - app.buttons["Your Account"].tap() + XCTAssertTrue(app.navigationBars.buttons["Your Account"].waitForExistence(timeout: 6.0)) + app.navigationBars.buttons["Your Account"].tap() XCTAssertTrue(app.buttons["License Information"].waitForExistence(timeout: 2)) app.buttons["License Information"].tap() // Test if the sheet opens by checking if the title of the sheet is present XCTAssertTrue(app.staticTexts["This project is licensed under the MIT License."].waitForExistence(timeout: 2)) - XCTAssertTrue(app.buttons["Repository Link"].waitForExistence(timeout: 2)) + XCTAssertTrue(app.buttons["Repository Link"].exists) } } diff --git a/TemplateApplicationUITests/OnboardingTests.swift b/TemplateApplicationUITests/OnboardingTests.swift index 10d23cb..438c510 100644 --- a/TemplateApplicationUITests/OnboardingTests.swift +++ b/TemplateApplicationUITests/OnboardingTests.swift @@ -12,9 +12,8 @@ import XCTHealthKit class OnboardingTests: XCTestCase { - override func setUpWithError() throws { - try super.setUpWithError() - + @MainActor + override func setUp() async throws { continueAfterFailure = false let app = XCUIApplication() @@ -22,7 +21,8 @@ class OnboardingTests: XCTestCase { app.deleteAndLaunch(withSpringboardAppName: "TemplateApplication") } - + + @MainActor func testOnboardingFlow() throws { let app = XCUIApplication() let email = "leland@onboarding.stanford.edu" @@ -32,7 +32,8 @@ class OnboardingTests: XCTestCase { app.assertOnboardingComplete() try app.assertAccountInformation(email: email) } - + + @MainActor func testOnboardingFlowRepeated() throws { let app = XCUIApplication() app.launchArguments = ["--showOnboarding", "--disableFirebase"] @@ -55,24 +56,16 @@ class OnboardingTests: XCTestCase { extension XCUIApplication { - func conductOnboardingIfNeeded(email: String = "leland@stanford.edu") throws { - let app = XCUIApplication() - - if app.staticTexts["Spezi\nTemplate Application"].waitForExistence(timeout: 5) { - try app.navigateOnboardingFlow(email: email) - } - } - fileprivate func navigateOnboardingFlow( email: String = "leland@stanford.edu", repeated skippedIfRepeated: Bool = false ) throws { try navigateOnboardingFlowWelcome() try navigateOnboardingFlowInterestingModules() - if staticTexts["Your Account"].waitForExistence(timeout: 5) { + if staticTexts["Your Account"].waitForExistence(timeout: 2.0) { try navigateOnboardingAccount(email: email) } - if staticTexts["Consent"].waitForExistence(timeout: 5) { + if staticTexts["Consent"].waitForExistence(timeout: 2.0) { try navigateOnboardingFlowConsent() } if !skippedIfRepeated { @@ -84,7 +77,7 @@ extension XCUIApplication { private func navigateOnboardingFlowWelcome() throws { XCTAssertTrue(staticTexts["Spezi\nTemplate Application"].waitForExistence(timeout: 5)) - XCTAssertTrue(buttons["Learn More"].waitForExistence(timeout: 2)) + XCTAssertTrue(buttons["Learn More"].exists) buttons["Learn More"].tap() } @@ -101,56 +94,58 @@ extension XCUIApplication { } private func navigateOnboardingAccount(email: String) throws { - if buttons["Logout"].waitForExistence(timeout: 2.0) { + if buttons["Logout"].exists { buttons["Logout"].tap() } - XCTAssertTrue(buttons["Signup"].waitForExistence(timeout: 2)) + XCTAssertTrue(buttons["Signup"].exists) buttons["Signup"].tap() XCTAssertTrue(staticTexts["Create a new Account"].waitForExistence(timeout: 2)) - + + XCTAssertTrue(collectionViews.textFields["E-Mail Address"].exists) try collectionViews.textFields["E-Mail Address"].enter(value: email) + XCTAssertTrue(collectionViews.secureTextFields["Password"].exists) try collectionViews.secureTextFields["Password"].enter(value: "StanfordRocks") + XCTAssertTrue(collectionViews.textFields["enter first name"].exists) try textFields["enter first name"].enter(value: "Leland") + XCTAssertTrue(collectionViews.textFields["enter last name"].exists) try textFields["enter last name"].enter(value: "Stanford") - XCTAssertTrue(collectionViews.buttons["Signup"].waitForExistence(timeout: 2)) + XCTAssertTrue(collectionViews.buttons["Signup"].exists) collectionViews.buttons["Signup"].tap() - sleep(3) - - if staticTexts["HealthKit Access"].waitForExistence(timeout: 5) && navigationBars.buttons["Back"].waitForExistence(timeout: 5) { + if staticTexts["Consent"].waitForExistence(timeout: 4.0) && navigationBars.buttons["Back"].exists { navigationBars.buttons["Back"].tap() XCTAssertTrue(staticTexts["Leland Stanford"].waitForExistence(timeout: 2)) - XCTAssertTrue(staticTexts[email].waitForExistence(timeout: 2)) - - XCTAssertTrue(buttons["Next"].waitForExistence(timeout: 2)) + XCTAssertTrue(staticTexts[email].exists) + + XCTAssertTrue(buttons["Next"].exists) buttons["Next"].tap() } } private func navigateOnboardingFlowConsent() throws { - XCTAssertTrue(staticTexts["Consent"].waitForExistence(timeout: 5)) - - XCTAssertTrue(staticTexts["First Name"].waitForExistence(timeout: 2)) + XCTAssertTrue(staticTexts["Consent"].waitForExistence(timeout: 2)) + + XCTAssertTrue(staticTexts["First Name"].exists) try textFields["Enter your first name ..."].enter(value: "Leland") - XCTAssertTrue(staticTexts["Last Name"].waitForExistence(timeout: 2)) + XCTAssertTrue(staticTexts["Last Name"].exists) try textFields["Enter your last name ..."].enter(value: "Stanford") - XCTAssertTrue(scrollViews["Signature Field"].waitForExistence(timeout: 2)) + XCTAssertTrue(scrollViews["Signature Field"].exists) scrollViews["Signature Field"].swipeRight() - XCTAssertTrue(buttons["I Consent"].waitForExistence(timeout: 2)) + XCTAssertTrue(buttons["I Consent"].exists) buttons["I Consent"].tap() } private func navigateOnboardingFlowHealthKitAccess() throws { XCTAssertTrue(staticTexts["HealthKit Access"].waitForExistence(timeout: 5)) - XCTAssertTrue(buttons["Grant Access"].waitForExistence(timeout: 2)) + XCTAssertTrue(buttons["Grant Access"].exists) buttons["Grant Access"].tap() try handleHealthKitAuthorization() @@ -159,7 +154,7 @@ extension XCUIApplication { private func navigateOnboardingFlowNotification() throws { XCTAssertTrue(staticTexts["Notifications"].waitForExistence(timeout: 5)) - XCTAssertTrue(buttons["Allow Notifications"].waitForExistence(timeout: 2)) + XCTAssertTrue(buttons["Allow Notifications"].exists) buttons["Allow Notifications"].tap() let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard") @@ -172,7 +167,7 @@ extension XCUIApplication { fileprivate func assertOnboardingComplete() { let tabBar = tabBars["Tab Bar"] XCTAssertTrue(tabBar.buttons["Schedule"].waitForExistence(timeout: 2)) - XCTAssertTrue(tabBar.buttons["Contacts"].waitForExistence(timeout: 2)) + XCTAssertTrue(tabBar.buttons["Contacts"].exists) } fileprivate func assertAccountInformation(email: String) throws { diff --git a/TemplateApplicationUITests/SchedulerTests.swift b/TemplateApplicationUITests/SchedulerTests.swift index c69d84e..fef49bb 100644 --- a/TemplateApplicationUITests/SchedulerTests.swift +++ b/TemplateApplicationUITests/SchedulerTests.swift @@ -11,9 +11,8 @@ import XCTestExtensions class SchedulerTests: XCTestCase { - override func setUpWithError() throws { - try super.setUpWithError() - + @MainActor + override func setUp() async throws { continueAfterFailure = false let app = XCUIApplication() @@ -21,11 +20,14 @@ class SchedulerTests: XCTestCase { app.deleteAndLaunch(withSpringboardAppName: "TemplateApplication") } - + + @MainActor func testScheduler() throws { let app = XCUIApplication() - - XCTAssertTrue(app.tabBars["Tab Bar"].buttons["Schedule"].waitForExistence(timeout: 2)) + + XCTAssertTrue(app.wait(for: .runningForeground, timeout: 2.0)) + + XCTAssertTrue(app.tabBars["Tab Bar"].buttons["Schedule"].exists) app.tabBars["Tab Bar"].buttons["Schedule"].tap() XCTAssertTrue(app.staticTexts["Start Questionnaire"].waitForExistence(timeout: 2))