diff --git a/.github/workflows/beta-deployment.yml b/.github/workflows/beta-deployment.yml index 15d053f0..f931c149 100644 --- a/.github/workflows/beta-deployment.yml +++ b/.github/workflows/beta-deployment.yml @@ -13,23 +13,81 @@ on: branches: - main workflow_dispatch: + inputs: + environment: + description: | + The GitHub deployment environment. + required: true + default: 'development' + type: choice + options: + - development + - staging + - production + workflow_call: + inputs: + environment: + description: | + The GitHub deployment environment. + required: false + type: string + default: staging + +concurrency: + group: Beta-Deployment + cancel-in-progress: false jobs: + determineenvironment: + name: Determine Environment + runs-on: ubuntu-latest + outputs: + environment: ${{ steps.determineenvironment.outputs.environment }} + steps: + - name: Checkout code + uses: actions/checkout@v4 + - name: Determine Environment + id: determineenvironment + run: | + if [[ -z "${{ inputs.environment }}" ]]; then + echo "environment=staging" >> $GITHUB_OUTPUT + else + echo "environment=${{ inputs.environment }}" >> $GITHUB_OUTPUT + fi + vars: + name: Inject Environment Variables In Deployment Workflow + needs: determineenvironment + runs-on: ubuntu-latest + environment: ${{ needs.determineenvironment.outputs.environment }} + outputs: + firebaseprojectid: ${{ vars.FIREBASE_PROJECT_ID }} + appidentifier: ${{ vars.APP_IDENTIFIER }} + provisioningProfileName: ${{ vars.PROVISIONING_PROFILE_NAME }} + steps: + - run: | + echo "Injecting Environment Variables In Deployment Workflow:" + echo "firebaseprojectid: ${{ vars.FIREBASE_PROJECT_ID }}" + echo "appidentifier: ${{ vars.APP_IDENTIFIER }}" + echo "provisioningProfileName: ${{ vars.PROVISIONING_PROFILE_NAME }}" buildandtest: name: Build and Test + needs: determineenvironment uses: ./.github/workflows/build-and-test.yml permissions: contents: read secrets: inherit iosapptestflightdeployment: name: iOS App TestFlight Deployment - needs: buildandtest + needs: [determineenvironment, vars, buildandtest] uses: StanfordBDHG/.github/.github/workflows/xcodebuild-or-fastlane.yml@v2 permissions: contents: read with: runsonlabels: '["macOS", "self-hosted"]' - googleserviceinfoplistpath: 'ENGAGEHF/Supporting Files/GoogleService-Info.plist' + environment: ${{ needs.determineenvironment.outputs.environment }} + googleserviceinfoplistpath: 'PAWS/Supporting Files/GoogleService-Info.plist' setupsigning: true - fastlanelane: beta - secrets: inherit + setupfirebaseemulator: true + firebaseemulatorimport: ./firebase --project ${{ needs.vars.outputs.firebaseprojectid }} + fastlanelane: deploy environment:"${{ needs.determineenvironment.outputs.environment }} appidentifier:"${{ needs.vars.outputs.appidentifier }} provisioningProfile:"${{ needs.vars.outputs.provisioningProfileName }}" + secrets: inherit \ No newline at end of file diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 141fe52b..91dba9b9 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -13,22 +13,11 @@ on: workflow_dispatch: workflow_call: +concurrency: + group: Build-and-Test-${{ github.ref }} + cancel-in-progress: true + jobs: - reuse_action: - name: REUSE Compliance Check - uses: StanfordBDHG/.github/.github/workflows/reuse.yml@v2 - permissions: - contents: read - swiftlint: - name: SwiftLint - uses: StanfordBDHG/.github/.github/workflows/swiftlint.yml@v2 - permissions: - contents: read - markdownlinkcheck: - name: Markdown Link Check - uses: StanfordBDHG/.github/.github/workflows/markdown-link-check.yml@v2 - permissions: - contents: read buildandtest: name: Build and Test uses: StanfordBDHG/.github/.github/workflows/xcodebuild-or-fastlane.yml@v2 diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml new file mode 100644 index 00000000..3f644d9c --- /dev/null +++ b/.github/workflows/static-analysis.yml @@ -0,0 +1,42 @@ +# +# This source file is part of the Stanford Spezi open source project +# +# SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) +# +# SPDX-License-Identifier: MIT +# + +name: Static Analysis + +on: + pull_request: + workflow_dispatch: + workflow_call: + +concurrency: + group: Static-Analysis-${{ github.ref }} + cancel-in-progress: true + +jobs: + reuse_action: + name: REUSE Compliance Check + uses: StanfordBDHG/.github/.github/workflows/reuse.yml@v2 + permissions: + contents: read + swiftlint: + name: SwiftLint + uses: StanfordBDHG/.github/.github/workflows/swiftlint.yml@v2 + permissions: + contents: read + # periphery: + # name: Periphery + # uses: StanfordBDHG/.github/.github/workflows/periphery.yml@v2 + # permissions: + # contents: read + # with: + # runsonlabels: '["macOS", "self-hosted"]' + markdownlinkcheck: + name: Markdown Link Check + uses: StanfordBDHG/.github/.github/workflows/markdown-link-check.yml@v2 + permissions: + contents: read diff --git a/.gitignore b/.gitignore index 00977e79..6e70a397 100644 --- a/.gitignore +++ b/.gitignore @@ -42,4 +42,4 @@ firebase-debug.log* firebase-debug.*.log* # Firebase cache -.firebase/ +firebase/ diff --git a/.periphery.yml b/.periphery.yml new file mode 100644 index 00000000..09784b73 --- /dev/null +++ b/.periphery.yml @@ -0,0 +1,15 @@ +# +# This source file is part of the ENGAGE-HF based on the Stanford Spezi Template Application project +# +# SPDX-FileCopyrightText: 2023 Stanford University +# +# SPDX-License-Identifier: MIT +# + +project: ENGAGEHF.xcodeproj +schemes: +- ENGAGEHF +targets: +- ENGAGEHF +- ENGAGEHFTests +- ENGAGEHFUITests diff --git a/.swiftlint.yml b/.swiftlint.yml index 8d77e234..0a60c155 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -381,6 +381,7 @@ excluded: # paths to ignore during linting. Takes precedence over `included`. - .swiftpm - .codeql - .derivedData + - fastlane/SnapshotHelper.swift closure_body_length: # Closure bodies should not span too many lines. - 35 # warning - default: 20 diff --git a/ENGAGEHF.xcodeproj/project.pbxproj b/ENGAGEHF.xcodeproj/project.pbxproj index 0f06be49..c7cd868b 100644 --- a/ENGAGEHF.xcodeproj/project.pbxproj +++ b/ENGAGEHF.xcodeproj/project.pbxproj @@ -12,6 +12,7 @@ 2117554D2CC1D3AB00A81E24 /* ExpandableSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2117554C2CC1D39900A81E24 /* ExpandableSection.swift */; }; 213AD98D2CBDF74A005C6D69 /* MedicationsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 213AD98C2CBDF744005C6D69 /* MedicationsSection.swift */; }; 213AD9942CBE02BB005C6D69 /* ColorKeyEntryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 213AD9922CBE02B1005C6D69 /* ColorKeyEntryView.swift */; }; + 21B93B782D039E310034EC86 /* ManagerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21B93B772D039E1B0034EC86 /* ManagerProtocol.swift */; }; 2F49B7762980407C00BCB272 /* Spezi in Frameworks */ = {isa = PBXBuildFile; productRef = 2F49B7752980407B00BCB272 /* Spezi */; }; 2F4E237E2989A2FE0013F3D9 /* LaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F4E237D2989A2FE0013F3D9 /* LaunchTests.swift */; }; 2F4E23832989D51F0013F3D9 /* ENGAGEHFTestingSetup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F4E23822989D51F0013F3D9 /* ENGAGEHFTestingSetup.swift */; }; @@ -23,7 +24,6 @@ 2FB099B12A875DF100B20952 /* FirebaseFirestore in Frameworks */ = {isa = PBXBuildFile; productRef = 2FB099B02A875DF100B20952 /* FirebaseFirestore */; }; 2FB099B62A875E2B00B20952 /* HealthKitOnFHIR in Frameworks */ = {isa = PBXBuildFile; productRef = 2FB099B52A875E2B00B20952 /* HealthKitOnFHIR */; }; 2FC3439029EE6346002D773C /* SocialSupportQuestionnaire.json in Resources */ = {isa = PBXBuildFile; fileRef = 2FE5DC5529EDD811004B9AB4 /* SocialSupportQuestionnaire.json */; }; - 2FC3439129EE6349002D773C /* AppIcon.png in Resources */ = {isa = PBXBuildFile; fileRef = 2FE5DC2A29EDD78D004B9AB4 /* AppIcon.png */; }; 2FC975A82978F11A00BA99FE /* Home.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FC975A72978F11A00BA99FE /* Home.swift */; }; 2FE5DC2629EDD38A004B9AB4 /* Contacts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FE5DC2529EDD38A004B9AB4 /* Contacts.swift */; }; 2FE5DC3729EDD7CA004B9AB4 /* OnboardingFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FE5DC3129EDD7CA004B9AB4 /* OnboardingFlow.swift */; }; @@ -46,6 +46,7 @@ 2FE5DCB129EE6107004B9AB4 /* AccountOnboarding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FE5DCAC29EE6107004B9AB4 /* AccountOnboarding.swift */; }; 2FF350E42C4F3F0B0095F08F /* Firebase+References.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FF350E32C4F3F0B0095F08F /* Firebase+References.swift */; }; 2FF53D8D2A8729D600042B76 /* ENGAGEHFStandard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FF53D8C2A8729D600042B76 /* ENGAGEHFStandard.swift */; }; + 2FF8F0522CC69015002C757A /* SnapshotHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FF8F0512CC69015002C757A /* SnapshotHelper.swift */; }; 4D05F26A2C5466B700201581 /* DosageGauge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D05F2692C5466B700201581 /* DosageGauge.swift */; }; 4D05F26D2C54696100201581 /* DosageGaugeStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D05F26C2C54696100201581 /* DosageGaugeStyle.swift */; }; 4D065BE02C09401700EBB3AE /* StudyApplicationListCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D065BDF2C09401700EBB3AE /* StudyApplicationListCard.swift */; }; @@ -202,6 +203,7 @@ 653A2551283387FE005D4D48 /* ENGAGEHF.swift in Sources */ = {isa = PBXBuildFile; fileRef = 653A2550283387FE005D4D48 /* ENGAGEHF.swift */; }; 653A255528338800005D4D48 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 653A255428338800005D4D48 /* Assets.xcassets */; }; 653A256228338800005D4D48 /* VitalsGraphAggregationUnitTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 653A256128338800005D4D48 /* VitalsGraphAggregationUnitTests.swift */; }; + 65D39ABD2CE497630040BF71 /* SpeziNotifications in Frameworks */ = {isa = PBXBuildFile; productRef = 65D39ABC2CE497630040BF71 /* SpeziNotifications */; }; 9733CFC62A8066DE001B7ABC /* SpeziOnboarding in Frameworks */ = {isa = PBXBuildFile; productRef = 2FE5DC8029EDD91D004B9AB4 /* SpeziOnboarding */; }; 9739A0C62AD7B5730084BEA5 /* FirebaseStorage in Frameworks */ = {isa = PBXBuildFile; productRef = 9739A0C52AD7B5730084BEA5 /* FirebaseStorage */; }; 97D73D6A2AD860AD00B47FA0 /* SpeziFirebaseStorage in Frameworks */ = {isa = PBXBuildFile; productRef = 97D73D692AD860AD00B47FA0 /* SpeziFirebaseStorage */; }; @@ -247,6 +249,7 @@ 2117554C2CC1D39900A81E24 /* ExpandableSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExpandableSection.swift; sourceTree = ""; }; 213AD98C2CBDF744005C6D69 /* MedicationsSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MedicationsSection.swift; sourceTree = ""; }; 213AD9922CBE02B1005C6D69 /* ColorKeyEntryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorKeyEntryView.swift; sourceTree = ""; }; + 21B93B772D039E1B0034EC86 /* ManagerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManagerProtocol.swift; sourceTree = ""; }; 2F4E237D2989A2FE0013F3D9 /* LaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchTests.swift; sourceTree = ""; }; 2F4E23822989D51F0013F3D9 /* ENGAGEHFTestingSetup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ENGAGEHFTestingSetup.swift; sourceTree = ""; }; 2F5E32BC297E05EA003432F8 /* ENGAGEHFDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ENGAGEHFDelegate.swift; sourceTree = ""; }; @@ -257,7 +260,6 @@ 2FC94CD4298B0A1D009C8209 /* ENGAGEHF.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = ENGAGEHF.xctestplan; sourceTree = ""; }; 2FC975A72978F11A00BA99FE /* Home.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Home.swift; sourceTree = ""; }; 2FE5DC2529EDD38A004B9AB4 /* Contacts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Contacts.swift; sourceTree = ""; }; - 2FE5DC2A29EDD78D004B9AB4 /* AppIcon.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = AppIcon.png; sourceTree = ""; }; 2FE5DC3129EDD7CA004B9AB4 /* OnboardingFlow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OnboardingFlow.swift; sourceTree = ""; }; 2FE5DC3229EDD7CA004B9AB4 /* InterestingModules.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InterestingModules.swift; sourceTree = ""; }; 2FE5DC3429EDD7CA004B9AB4 /* Welcome.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Welcome.swift; sourceTree = ""; }; @@ -267,6 +269,7 @@ 2FE5DCAC29EE6107004B9AB4 /* AccountOnboarding.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountOnboarding.swift; sourceTree = ""; }; 2FF350E32C4F3F0B0095F08F /* Firebase+References.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Firebase+References.swift"; sourceTree = ""; }; 2FF53D8C2A8729D600042B76 /* ENGAGEHFStandard.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ENGAGEHFStandard.swift; sourceTree = ""; }; + 2FF8F0512CC69015002C757A /* SnapshotHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = SnapshotHelper.swift; path = fastlane/SnapshotHelper.swift; sourceTree = SOURCE_ROOT; }; 4D05F2692C5466B700201581 /* DosageGauge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DosageGauge.swift; sourceTree = ""; }; 4D05F26C2C54696100201581 /* DosageGaugeStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DosageGaugeStyle.swift; sourceTree = ""; }; 4D065BDF2C09401700EBB3AE /* StudyApplicationListCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StudyApplicationListCard.swift; sourceTree = ""; }; @@ -450,6 +453,7 @@ 2FB099B62A875E2B00B20952 /* HealthKitOnFHIR in Frameworks */, 56E7083B2BB06F6F00B08F0A /* SwiftPackageList in Frameworks */, A977DD5D2C26279E00A2A8E5 /* SpeziBluetoothServices in Frameworks */, + 65D39ABD2CE497630040BF71 /* SpeziNotifications in Frameworks */, 4D49AB022BC9D50400C77310 /* SpeziBluetooth in Frameworks */, 56E708352BB06B7100B08F0A /* SpeziLicense in Frameworks */, 2FE5DC8A29EDD972004B9AB4 /* SpeziLocalStorage in Frameworks */, @@ -532,7 +536,6 @@ 2FE5DC2D29EDD792004B9AB4 /* Resources */ = { isa = PBXGroup; children = ( - 2FE5DC2A29EDD78D004B9AB4 /* AppIcon.png */, 653A255428338800005D4D48 /* Assets.xcassets */, 2FA0BFEC2ACC977500E0EF83 /* Localizable.xcstrings */, 2FE5DC5529EDD811004B9AB4 /* SocialSupportQuestionnaire.json */, @@ -570,6 +573,7 @@ 4D081A212C6C2BCB00E13BC8 /* Managers */ = { isa = PBXGroup; children = ( + 21B93B772D039E1B0034EC86 /* ManagerProtocol.swift */, 4D8CE0F42C752D1C00560327 /* VideoManager */, 4D081A242C6C2C4500E13BC8 /* UserMetaDataManager */, 4D081A232C6C2BF200E13BC8 /* MedicationsManager */, @@ -711,8 +715,8 @@ 4D62BF372C6A97490088C414 /* NotificationManager */ = { isa = PBXGroup; children = ( - 4D8CE0FA2C753A4600560327 /* NotificationManager.swift */, 4D8CE0FB2C753A4600560327 /* NotificationRegistrationSchema.swift */, + 4D8CE0FA2C753A4600560327 /* NotificationManager.swift */, ); path = NotificationManager; sourceTree = ""; @@ -761,6 +765,7 @@ 4D84027B2C4EE61C00817495 /* HelperFunctions */ = { isa = PBXGroup; children = ( + 2FF8F0512CC69015002C757A /* SnapshotHelper.swift */, 4D8402792C4EE56000817495 /* XCUIApplication+GoTo.swift */, 4D8402832C4F11CB00817495 /* XCUIApplication+DeleteMeasurements.swift */, ); @@ -1039,10 +1044,10 @@ children = ( 4D2D1D732C2CC89600ABDC19 /* Protocols+Aliases */, 4DA20B562C25FE2B00715AA2 /* Extensions */, - 4DA20B4A2C249E7E00715AA2 /* VitalsManager.swift */, 4DF506202C2E1C89003E7EFB /* SymptomScore.swift */, 4D34B3EC2C404C61006F0D40 /* VitalsUnit.swift */, 4D4670C32C792429004DEAC4 /* FHIRObservationToHKSampleConverter.swift */, + 4DA20B4A2C249E7E00715AA2 /* VitalsManager.swift */, ); path = VitalsManager; sourceTree = ""; @@ -1200,6 +1205,7 @@ A977DD552C25E2DA00A2A8E5 /* SpeziDevicesUI */, A977DD5C2C26279E00A2A8E5 /* SpeziBluetoothServices */, 9B0723AA2C8F6AE700D901E5 /* FirebaseMessaging */, + 65D39ABC2CE497630040BF71 /* SpeziNotifications */, ); productName = ENGAGEHF; productReference = 653A254D283387FE005D4D48 /* ENGAGEHF.app */; @@ -1299,6 +1305,7 @@ 4D49AAFC2BC9D50400C77310 /* XCRemoteSwiftPackageReference "SpeziBluetooth" */, A9A0BF012C13121D00B8F3F3 /* XCRemoteSwiftPackageReference "SpeziNetworking" */, A9623C022C17A3F500189BA1 /* XCRemoteSwiftPackageReference "SpeziDevices" */, + 65D39ABB2CE497630040BF71 /* XCRemoteSwiftPackageReference "SpeziNotifications" */, ); productRefGroup = 653A254E283387FE005D4D48 /* Products */; projectDirPath = ""; @@ -1316,7 +1323,6 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 2FC3439129EE6349002D773C /* AppIcon.png in Resources */, 653A255528338800005D4D48 /* Assets.xcassets in Resources */, 2FC3439029EE6346002D773C /* SocialSupportQuestionnaire.json in Resources */, 2FA0BFED2ACC977500E0EF83 /* Localizable.xcstrings in Resources */, @@ -1445,6 +1451,7 @@ 4D73A50E2C5807B500CCEC46 /* CapsuleStack.swift in Sources */, 4D2D1D682C2C82A200ABDC19 /* FHIRPeriod+GetDates.swift in Sources */, 4D2D1D762C2CC8B200ABDC19 /* ValueXProtocol.swift in Sources */, + 21B93B782D039E310034EC86 /* ManagerProtocol.swift in Sources */, 4D4670C72C793327004DEAC4 /* SeriesTarget.swift in Sources */, 4DA20B542C25F8A600715AA2 /* FHIRDateTime+DateComponents.swift in Sources */, 4D081A282C6C2C8400E13BC8 /* UserMetaDataManager.swift in Sources */, @@ -1539,6 +1546,7 @@ 4D8402842C4F11CB00817495 /* XCUIApplication+DeleteMeasurements.swift in Sources */, 4D8402772C4EE52000817495 /* AddMeasurementUITests.swift in Sources */, 4D8AD2E12C4B5D1600CB4F3E /* HeartHealthUITests.swift in Sources */, + 2FF8F0522CC69015002C757A /* SnapshotHelper.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1668,7 +1676,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0; + MARKETING_VERSION = 2.0; PRODUCT_BUNDLE_IDENTIFIER = edu.stanford.bdh.engagehf; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1864,7 +1872,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0; + MARKETING_VERSION = 2.0; PRODUCT_BUNDLE_IDENTIFIER = edu.stanford.bdh.engagehf; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1888,7 +1896,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_TEAM = ""; - "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 637867499T; + "DEVELOPMENT_TEAM[sdk=iphoneos*]" = CQRZ4E7K9U; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = "ENGAGEHF/Supporting Files/Info.plist"; @@ -1908,11 +1916,11 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = edu.stanford.bdh.engagehf; + MARKETING_VERSION = 2.0; + PRODUCT_BUNDLE_IDENTIFIER = edu.stanford.hrtex; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; - "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "Stanford BDHG ENGAGE-HF"; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "ENGAGE-HF - Biodesign Digital Health"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; @@ -2036,7 +2044,7 @@ repositoryURL = "https://github.com/StanfordSpezi/Spezi"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 1.7.0; + minimumVersion = 1.8.0; }; }; 2F66D20D2BB723180010D555 /* XCRemoteSwiftPackageReference "SwiftLint" */ = { @@ -2044,7 +2052,7 @@ repositoryURL = "https://github.com/realm/SwiftLint.git"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 0.55.0; + minimumVersion = 0.57.0; }; }; 2FB099B42A875E2B00B20952 /* XCRemoteSwiftPackageReference "HealthKitOnFHIR" */ = { @@ -2060,7 +2068,7 @@ repositoryURL = "https://github.com/StanfordSpezi/SpeziAccount.git"; requirement = { kind = upToNextMajorVersion; - minimumVersion = "2.0.0-beta.8"; + minimumVersion = 2.1.1; }; }; 2FE5DC6529EDD894004B9AB4 /* XCRemoteSwiftPackageReference "SpeziContact" */ = { @@ -2068,7 +2076,7 @@ repositoryURL = "https://github.com/StanfordSpezi/SpeziContact.git"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 1.0.0; + minimumVersion = 1.0.2; }; }; 2FE5DC7029EDD8D3004B9AB4 /* XCRemoteSwiftPackageReference "SpeziHealthKit" */ = { @@ -2084,7 +2092,7 @@ repositoryURL = "https://github.com/StanfordSpezi/SpeziFirebase.git"; requirement = { kind = upToNextMajorVersion; - minimumVersion = "2.0.0-beta.4"; + minimumVersion = 2.0.1; }; }; 2FE5DC8229EDD934004B9AB4 /* XCRemoteSwiftPackageReference "SpeziQuestionnaire" */ = { @@ -2092,7 +2100,7 @@ repositoryURL = "https://github.com/StanfordSpezi/SpeziQuestionnaire.git"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 1.0.0; + minimumVersion = 1.2.3; }; }; 2FE5DC8829EDD972004B9AB4 /* XCRemoteSwiftPackageReference "SpeziStorage" */ = { @@ -2100,7 +2108,7 @@ repositoryURL = "https://github.com/StanfordSpezi/SpeziStorage.git"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 1.0.0; + minimumVersion = 1.2.1; }; }; 2FE5DC8D29EDD980004B9AB4 /* XCRemoteSwiftPackageReference "SpeziViews" */ = { @@ -2108,7 +2116,7 @@ repositoryURL = "https://github.com/StanfordSpezi/SpeziViews.git"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 1.5.0; + minimumVersion = 1.7.0; }; }; 2FE5DC9029EDD9C3004B9AB4 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */ = { @@ -2116,7 +2124,7 @@ repositoryURL = "https://github.com/firebase/firebase-ios-sdk.git"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 11.0.0; + minimumVersion = 11.5.0; }; }; 2FE5DC9729EDD9D9004B9AB4 /* XCRemoteSwiftPackageReference "XCTestExtensions" */ = { @@ -2124,7 +2132,7 @@ repositoryURL = "https://github.com/StanfordBDHG/XCTestExtensions.git"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 1.0.0; + minimumVersion = 1.1.0; }; }; 2FE5DC9A29EDD9EF004B9AB4 /* XCRemoteSwiftPackageReference "XCTHealthKit" */ = { @@ -2140,7 +2148,7 @@ repositoryURL = "https://github.com/StanfordSpezi/SpeziBluetooth.git"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 3.0.0; + minimumVersion = 3.1.0; }; }; 5661551B2AB8384200209B80 /* XCRemoteSwiftPackageReference "swift-package-list" */ = { @@ -2156,7 +2164,15 @@ repositoryURL = "https://github.com/StanfordSpezi/SpeziLicense"; requirement = { kind = upToNextMinorVersion; - minimumVersion = 0.1.0; + minimumVersion = 0.1.1; + }; + }; + 65D39ABB2CE497630040BF71 /* XCRemoteSwiftPackageReference "SpeziNotifications" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/StanfordSpezi/SpeziNotifications.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.0.2; }; }; 97F466E62A76BBEE005DC9B4 /* XCRemoteSwiftPackageReference "SpeziOnboarding" */ = { @@ -2164,7 +2180,7 @@ repositoryURL = "https://github.com/StanfordSpezi/SpeziOnboarding"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 1.0.0; + minimumVersion = 1.2.2; }; }; A9623C022C17A3F500189BA1 /* XCRemoteSwiftPackageReference "SpeziDevices" */ = { @@ -2172,7 +2188,7 @@ repositoryURL = "https://github.com/StanfordSpezi/SpeziDevices.git"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 1.3.0; + minimumVersion = 1.4.1; }; }; A9A0BF012C13121D00B8F3F3 /* XCRemoteSwiftPackageReference "SpeziNetworking" */ = { @@ -2180,7 +2196,7 @@ repositoryURL = "https://github.com/StanfordSpezi/SpeziNetworking"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 2.1.0; + minimumVersion = 2.2.0; }; }; /* End XCRemoteSwiftPackageReference section */ @@ -2306,6 +2322,11 @@ package = 5661551B2AB8384200209B80 /* XCRemoteSwiftPackageReference "swift-package-list" */; productName = "plugin:SwiftPackageListPlugin"; }; + 65D39ABC2CE497630040BF71 /* SpeziNotifications */ = { + isa = XCSwiftPackageProductDependency; + package = 65D39ABB2CE497630040BF71 /* XCRemoteSwiftPackageReference "SpeziNotifications" */; + productName = SpeziNotifications; + }; 9739A0C52AD7B5730084BEA5 /* FirebaseStorage */ = { isa = XCSwiftPackageProductDependency; package = 2FE5DC9029EDD9C3004B9AB4 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */; diff --git a/ENGAGEHF.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ENGAGEHF.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 1b1a331c..d71af38a 100644 --- a/ENGAGEHF.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/ENGAGEHF.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "bf5084f3a1640b549abaeb7502f3d1650efe31a71ce63ae49ff0fb7732a037e2", + "originHash" : "2f8235f304c1dd7bcfc68f692f99b4029356877d29f596e8787777ad4f475cb3", "pins" : [ { "identity" : "abseil-cpp-binary", @@ -13,7 +13,7 @@ { "identity" : "antlr4", "kind" : "remoteSourceControl", - "location" : "https://github.com/antlr/antlr4", + "location" : "https://github.com/antlr/antlr4.git", "state" : { "revision" : "cc82115a4e7f53d71d9d905caa2c2dfa4da58899", "version" : "4.13.2" @@ -24,8 +24,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/google/app-check.git", "state" : { - "revision" : "21fe1af9be463a359aaf8d96789ef73fc3760d09", - "version" : "11.0.1" + "revision" : "61b85103a1aeed8218f17c794687781505fbbef5", + "version" : "11.2.0" } }, { @@ -60,8 +60,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/firebase/firebase-ios-sdk.git", "state" : { - "revision" : "1fc52ab0e172e7c5a961f975a76c2611f4f22852", - "version" : "11.2.0" + "revision" : "2e02253fd1ce99145bcbf1bb367ccf61bd0ca46b", + "version" : "11.6.0" } }, { @@ -69,8 +69,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/google/GoogleAppMeasurement.git", "state" : { - "revision" : "07a2f57d147d2bf368a0d2dcb5579ff082d9e44f", - "version" : "11.1.0" + "revision" : "4f234bcbdae841d7015258fbbf8e7743a39b8200", + "version" : "11.4.0" } }, { @@ -105,8 +105,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/google/gtm-session-fetcher.git", "state" : { - "revision" : "a2ab612cb980066ee56d90d60d8462992c07f24b", - "version" : "3.5.0" + "revision" : "5cfe5f090c982de9c58605d2a82a4fc77b774fbd", + "version" : "4.1.0" } }, { @@ -114,8 +114,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/StanfordBDHG/HealthKitOnFHIR.git", "state" : { - "revision" : "87a9257e6fa37407f3437e4a0bf21dd09a4ea7c5", - "version" : "0.2.11" + "revision" : "a1a71254a75c6a3b18a0b356fd8f76bc489a3030", + "version" : "0.2.12" } }, { @@ -168,8 +168,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/StanfordBDHG/ResearchKitOnFHIR", "state" : { - "revision" : "23dda93dc652b5c2753b5cbf989faf8185fd17f1", - "version" : "2.0.1" + "revision" : "d8d8b0d01599ad8a5a8397d10a99073728e6ae9b", + "version" : "2.0.2" } }, { @@ -186,8 +186,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/StanfordSpezi/Spezi", "state" : { - "revision" : "d5c8eec55255fd753367422c49db1ec16610c48b", - "version" : "1.7.4" + "revision" : "4513a697572e8e1faea1e0ee52e6fad4b8d3dd8d", + "version" : "1.8.0" } }, { @@ -195,8 +195,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/StanfordSpezi/SpeziAccount.git", "state" : { - "revision" : "397a55a7b0f108088d3db510ee48fc974a322c87", - "version" : "2.0.0-beta.8" + "revision" : "37df11e8f65b9aa0a3aaf0d4bc3e65e87b425637", + "version" : "2.1.1" } }, { @@ -204,8 +204,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/StanfordSpezi/SpeziBluetooth.git", "state" : { - "revision" : "8ee8ba902cff833aa6a6062fc8433e5d0e0338f3", - "version" : "3.0.1" + "revision" : "977cc675d8c2dca51eabd52dc0e9476702c678eb", + "version" : "3.1.0" } }, { @@ -222,8 +222,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/StanfordSpezi/SpeziDevices.git", "state" : { - "revision" : "dc8dcd53773b9bb4041870b0f65cec32f7ed5108", - "version" : "1.3.0" + "revision" : "397c0e8217350a32621a3e6abdec0d86f8c651a6", + "version" : "1.4.1" } }, { @@ -231,8 +231,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/StanfordSpezi/SpeziFirebase.git", "state" : { - "revision" : "5aae93f567091b29f7683863baa2241f7c95290d", - "version" : "2.0.0-beta.4" + "revision" : "5dd57f9de42c02d6a94f3af4d8cf3d9b81ec6661", + "version" : "2.0.1" } }, { @@ -240,8 +240,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/StanfordSpezi/SpeziFoundation", "state" : { - "revision" : "17bd0e03e69e0cc722d4cd28148d04443f4e2aba", - "version" : "2.0.0-beta.2" + "revision" : "5b4ad1b343154b52a68c33a6bfe02d9cb07cb9dc", + "version" : "2.0.0" } }, { @@ -267,8 +267,17 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/StanfordSpezi/SpeziNetworking", "state" : { - "revision" : "26f1ed49e1916ae4fdc1bd033127a873a9e6dd0a", - "version" : "2.1.3" + "revision" : "89fad797897bb741fc148027859c1bab3129999a", + "version" : "2.2.0" + } + }, + { + "identity" : "spezinotifications", + "kind" : "remoteSourceControl", + "location" : "https://github.com/StanfordSpezi/SpeziNotifications.git", + "state" : { + "revision" : "7f24fce6b969d0f1a7bcc0e228af1c01e55fb59f", + "version" : "1.0.2" } }, { @@ -294,8 +303,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/StanfordSpezi/SpeziStorage.git", "state" : { - "revision" : "60962375bbce7cc0599bc3da4ebda08231922371", - "version" : "1.2.0" + "revision" : "0f4a54430e51f82d29da63a7ce5f61bad7dfb9cd", + "version" : "1.2.1" } }, { @@ -303,14 +312,14 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/StanfordSpezi/SpeziViews.git", "state" : { - "revision" : "427f4f3a7acb0e00ea11c4c3ca7b60e36d2557a0", - "version" : "1.6.0" + "revision" : "f87514406bb57ae67d0040eec5454fff55104143", + "version" : "1.7.0" } }, { "identity" : "swift-argument-parser", "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-argument-parser", + "location" : "https://github.com/apple/swift-argument-parser.git", "state" : { "revision" : "41982a3656a71c768319979febd796c6fd111d5c", "version" : "1.5.0" @@ -328,10 +337,10 @@ { "identity" : "swift-collections", "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-collections", + "location" : "https://github.com/apple/swift-collections.git", "state" : { - "revision" : "9bf03ff58ce34478e66aaee630e491823326fd06", - "version" : "1.1.3" + "revision" : "671108c96644956dddcd89dd59c203dcdb36cec7", + "version" : "1.1.4" } }, { @@ -339,8 +348,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-nio.git", "state" : { - "revision" : "9746cf80e29edfef2a39924a66731249223f42a3", - "version" : "2.72.0" + "revision" : "dca6594f65308c761a9c409e09fbf35f48d50d34", + "version" : "2.77.0" } }, { @@ -348,8 +357,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/FelixHerrmann/swift-package-list", "state" : { - "revision" : "60b77bea44f5b2f7369aecfcd993f7672e09602a", - "version" : "4.3.0" + "revision" : "5e954ec39ce2374ff28a38224fd4e6bba7c57cdc", + "version" : "4.4.2" } }, { @@ -357,17 +366,17 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-protobuf.git", "state" : { - "revision" : "edb6ed4919f7756157fe02f2552b7e3850a538e5", - "version" : "1.28.1" + "revision" : "ebc7251dd5b37f627c93698e4374084d98409633", + "version" : "1.28.2" } }, { "identity" : "swift-syntax", "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-syntax.git", + "location" : "https://github.com/swiftlang/swift-syntax.git", "state" : { - "revision" : "2bc86522d115234d1f588efe2bcb4ce4be8f8b82", - "version" : "510.0.3" + "revision" : "cb53fa1bd3219b0b23ded7dfdd3b2baff266fd25", + "version" : "600.0.0" } }, { @@ -375,8 +384,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-system.git", "state" : { - "revision" : "d2ba781702a1d8285419c15ee62fd734a9437ff5", - "version" : "1.3.2" + "revision" : "c8a44d836fe7913603e246acab7c528c2e780168", + "version" : "1.4.0" } }, { @@ -384,8 +393,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/realm/SwiftLint.git", "state" : { - "revision" : "b515723b16eba33f15c4677ee65f3fef2ce8c255", - "version" : "0.55.1" + "revision" : "25f2776977e663305bee71309ea1e34d435065f1", + "version" : "0.57.1" } }, { @@ -411,8 +420,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/StanfordBDHG/XCTestExtensions.git", "state" : { - "revision" : "aad6c161a09d658f30c7170deb4a61e8916a4a4c", - "version" : "1.0.0" + "revision" : "5379d70249cae926927105bfb6686770f03ee5b9", + "version" : "1.1.0" } }, { @@ -427,7 +436,7 @@ { "identity" : "xctruntimeassertions", "kind" : "remoteSourceControl", - "location" : "https://github.com/StanfordBDHG/XCTRuntimeAssertions", + "location" : "https://github.com/StanfordBDHG/XCTRuntimeAssertions.git", "state" : { "revision" : "f560ec8410af032dd485ca9386e8c2b5d3e1a1f8", "version" : "1.1.3" diff --git a/ENGAGEHF.xcodeproj/xcshareddata/xcschemes/ENGAGEHF.xcscheme b/ENGAGEHF.xcodeproj/xcshareddata/xcschemes/ENGAGEHF.xcscheme index eca0e38c..9ee9963c 100644 --- a/ENGAGEHF.xcodeproj/xcshareddata/xcschemes/ENGAGEHF.xcscheme +++ b/ENGAGEHF.xcodeproj/xcshareddata/xcschemes/ENGAGEHF.xcscheme @@ -83,7 +83,7 @@ + isEnabled = "NO"> + isEnabled = "NO"> () -> Binding where Value == V? { - Binding { - wrappedValue != nil - } set: { newValue in - if newValue { - preconditionFailure("Tried setting wrappedValue to `true` on a binding built using `Binding.exists()`.") } else { - wrappedValue = nil + Image(.engagehfIcon) + .resizable() + .scaledToFit() + .frame(width: 128, height: 128) + .clipShape(RoundedRectangle(cornerRadius: 32)) + .accessibilityLabel("ENGAGE-HF Application Loading Screen") } } + .accountRequired( + accountSetupIsComplete: { _ in + expectedSheetContent == nil + }, + setupSheet: { + switch expectedSheetContent { + case .onboarding: + OnboardingFlow() + .interactiveDismissDisabled(true) + case .auth: + AuthFlow() + .interactiveDismissDisabled(true) + case .none: + EmptyView() + } + } + ) } } diff --git a/ENGAGEHF/ENGAGEHFDelegate.swift b/ENGAGEHF/ENGAGEHFDelegate.swift index 081bebf5..30affdc5 100644 --- a/ENGAGEHF/ENGAGEHFDelegate.swift +++ b/ENGAGEHF/ENGAGEHFDelegate.swift @@ -28,7 +28,12 @@ class ENGAGEHFDelegate: SpeziAppDelegate { Configuration(standard: ENGAGEHFStandard()) { if !FeatureFlags.disableFirebase { AccountConfiguration( - service: FirebaseAccountService(providers: [.emailAndPassword, .signInWithApple], emulatorSettings: accountEmulator), + service: FirebaseAccountService( + providers: [ + .emailAndPassword + ], + emulatorSettings: accountEmulator + ), storageProvider: FirestoreAccountStorage(storeIn: Firestore.userCollection, mapping: [ "dateOfBirth": AccountKeys.dateOfBirth, "invitationCode": AccountKeys.invitationCode, @@ -81,7 +86,6 @@ class ENGAGEHFDelegate: SpeziAppDelegate { MedicationsManager() VideoManager() - OnboardingDataSource() InvitationCodeModule() ConfigureTipKit() diff --git a/ENGAGEHF/ENGAGEHFStandard.swift b/ENGAGEHF/ENGAGEHFStandard.swift index 80c974c1..4d47136f 100644 --- a/ENGAGEHF/ENGAGEHFStandard.swift +++ b/ENGAGEHF/ENGAGEHFStandard.swift @@ -22,7 +22,7 @@ import SpeziQuestionnaire import SwiftUI -actor ENGAGEHFStandard: Standard, EnvironmentAccessible, OnboardingConstraint { +actor ENGAGEHFStandard: Standard, EnvironmentAccessible { @Application(\.logger) private var logger @Dependency(Account.self) private var account: Account? @@ -89,37 +89,4 @@ actor ENGAGEHFStandard: Standard, EnvironmentAccessible, OnboardingConstraint { throw FirestoreError(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`. - func store(consent: PDFDocument) async { - 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.") - return - } - - let filePath = basePath.appending(path: "consent.pdf") - consent.write(to: filePath) - - return - } - - do { - guard let consentData = consent.dataRepresentation() else { - logger.error("Could not store consent form.") - return - } - - let metadata = StorageMetadata() - metadata.contentType = "application/pdf" - _ = try await Storage.userBucketReference(for: accountId) - .child("consent") - .child("consent.pdf") - .putDataAsync(consentData, metadata: metadata) - } catch { - logger.error("Could not store consent form: \(error)") - } - } } diff --git a/ENGAGEHF/HealthSummary/HealthSummaryView.swift b/ENGAGEHF/HealthSummary/HealthSummaryView.swift index d05a3c1f..1fee6cfc 100644 --- a/ENGAGEHF/HealthSummary/HealthSummaryView.swift +++ b/ENGAGEHF/HealthSummary/HealthSummaryView.swift @@ -35,7 +35,7 @@ struct HealthSummaryView: View { ToolbarItem(placement: .confirmationAction) { ShareLink( item: healthSummaryDocument, - preview: SharePreview("Health Summary", image: Image(.healthSummary)) + preview: SharePreview("Health Summary", image: Image(.engagehfIcon)) ) .accessibilityLabel("Share Link") } diff --git a/ENGAGEHF/HeartHealth/Symptoms/SymptomsGraphSection.swift b/ENGAGEHF/HeartHealth/Symptoms/SymptomsGraphSection.swift index fcb2ebd0..5ca77575 100644 --- a/ENGAGEHF/HeartHealth/Symptoms/SymptomsGraphSection.swift +++ b/ENGAGEHF/HeartHealth/Symptoms/SymptomsGraphSection.swift @@ -58,6 +58,7 @@ struct SymptomsGraphSection: View { content: { VitalsGraph(data: graphData, options: options) .environment(\.customChartYAxis, symptomsType == .dizziness ? .dizzinessYAxisModifier : .percentageYAxisModifier ) + .disabled(vitalsManager.symptomHistory.isEmpty) }, header: { SymptomsPicker(symptomsType: $symptomsType) diff --git a/ENGAGEHF/HeartHealth/Vitals/VitalsGraphSection.swift b/ENGAGEHF/HeartHealth/Vitals/VitalsGraphSection.swift index 82c3049a..ac2caf3e 100644 --- a/ENGAGEHF/HeartHealth/Vitals/VitalsGraphSection.swift +++ b/ENGAGEHF/HeartHealth/Vitals/VitalsGraphSection.swift @@ -43,6 +43,7 @@ struct VitalsGraphSection: View { dateResolution: granularity.defaultDateUnit, targetValue: vitalsType == .weight ? vitalsManager.latestDryWeight : nil ) + .disabled(data.isEmpty) }, header: { HStack { diff --git a/ENGAGEHF/Managers/ManagerProtocol.swift b/ENGAGEHF/Managers/ManagerProtocol.swift new file mode 100644 index 00000000..7ecdc805 --- /dev/null +++ b/ENGAGEHF/Managers/ManagerProtocol.swift @@ -0,0 +1,21 @@ +// +// This source file is part of the ENGAGE-HF project based on the Stanford Spezi Template Application project +// +// SPDX-FileCopyrightText: 2023 Stanford University +// +// SPDX-License-Identifier: MIT +// + +import Spezi + + +/// A protocol to force refresh of the content of conforming Managers. +protocol RefreshableContent { + @MainActor + func refreshContent() +} + + +/// A `Manager` is a `Spezi` `Module` that is environment accessible, default initializable (for dependencies between modules), +/// and supportive of content refreshes. +protocol Manager: Module, EnvironmentAccessible, DefaultInitializable, RefreshableContent {} diff --git a/ENGAGEHF/Managers/MedicationsManager/MedicationsManager.swift b/ENGAGEHF/Managers/MedicationsManager/MedicationsManager.swift index 73f10835..d9a97207 100644 --- a/ENGAGEHF/Managers/MedicationsManager/MedicationsManager.swift +++ b/ENGAGEHF/Managers/MedicationsManager/MedicationsManager.swift @@ -18,7 +18,7 @@ import SpeziFirebaseAccount /// Decodes the current user's medication recommendations from Firestore to an easily displayed internal representation @Observable @MainActor -class MedicationsManager: Module, EnvironmentAccessible { +final class MedicationsManager: Manager { @ObservationIgnored @StandardActor private var standard: ENGAGEHFStandard @ObservationIgnored @Dependency(Account.self) private var account: Account? @@ -33,6 +33,9 @@ class MedicationsManager: Module, EnvironmentAccessible { var medications: [MedicationDetails] = [] + nonisolated init() {} + + func configure() { if ProcessInfo.processInfo.isPreviewSimulator { setupPreview() @@ -65,6 +68,11 @@ class MedicationsManager: Module, EnvironmentAccessible { } + func refreshContent() { + updateSnapshotListener(for: account?.details) + } + + /// Call on sign-in. Registers a snapshot listener to the current user's medicationRecommendations collection and decodes the medications found there. private func updateSnapshotListener(for details: AccountDetails?) { logger.info("Initializing medications snapshot listener...") diff --git a/ENGAGEHF/Managers/MessageManager/MessageManager.swift b/ENGAGEHF/Managers/MessageManager/MessageManager.swift index e80b7f38..aa75b13c 100644 --- a/ENGAGEHF/Managers/MessageManager/MessageManager.swift +++ b/ENGAGEHF/Managers/MessageManager/MessageManager.swift @@ -21,7 +21,7 @@ import SpeziFirebaseAccount /// On sign-in, adds a snapshot listener to the user's messages collection @Observable @MainActor -final class MessageManager: Module, EnvironmentAccessible, DefaultInitializable { +final class MessageManager: Manager { @ObservationIgnored @StandardActor var standard: ENGAGEHFStandard @ObservationIgnored @Dependency(Account.self) private var account: Account? @@ -69,10 +69,14 @@ final class MessageManager: Module, EnvironmentAccessible, DefaultInitializable } + func refreshContent() { + updateSnapshotListener(for: account?.details) + } + + /// Call on initialization and sign-in of user /// /// Creates a snapshot listener to save new messages to the manager as they are added to the user's directory in Firebase - @MainActor private func updateSnapshotListener(for details: AccountDetails?) { logger.info("Initializing message snapshot listener...") @@ -111,7 +115,7 @@ final class MessageManager: Module, EnvironmentAccessible, DefaultInitializable } } - @MainActor + func dismiss(_ message: Message, didPerformAction: Bool) async { logger.debug("Dismissing message with id: \(message.id ?? "nil")") @@ -154,6 +158,7 @@ final class MessageManager: Module, EnvironmentAccessible, DefaultInitializable logger.debug("Successfully dismissed message (\(messageId)).") } + deinit { _notificationTask?.cancel() } @@ -164,7 +169,6 @@ final class MessageManager: Module, EnvironmentAccessible, DefaultInitializable extension MessageManager { /// Adds a mock message to self.messages /// Used for testing in previews - @MainActor func addMockMessage(dismissible: Bool = true, action: MessageAction = .showHealthSummary) { let mockMessage = Message( title: "Medication Change", @@ -179,7 +183,6 @@ extension MessageManager { } - @MainActor private func injectTestMessages() { self.messages = [ // With play video action, with description, is dismissible diff --git a/ENGAGEHF/Managers/NavigationManager/NavigationManager.swift b/ENGAGEHF/Managers/NavigationManager/NavigationManager.swift index d21d6420..d7d84176 100644 --- a/ENGAGEHF/Managers/NavigationManager/NavigationManager.swift +++ b/ENGAGEHF/Managers/NavigationManager/NavigationManager.swift @@ -18,7 +18,7 @@ import SwiftUI /// Wraps an environment accessible and observable stack for use in navigating between views @MainActor @Observable -class NavigationManager: Module, EnvironmentAccessible { +final class NavigationManager: Manager { @ObservationIgnored @Dependency(AccountNotifications.self) private var accountNotifications: AccountNotifications? @ObservationIgnored @Dependency(VideoManager.self) private var videoManager @@ -36,10 +36,17 @@ class NavigationManager: Module, EnvironmentAccessible { private var notificationTask: Task? + + nonisolated init() {} + + // On sign in, reinitialize to an empty navigation path func configure() { guard let accountNotifications else { - preconditionFailure("Expected account notifications to be availble.") + guard FeatureFlags.disableFirebase else { + preconditionFailure("Expected account notifications to be availble.") + } + return } notificationTask = Task.detached { @MainActor [weak self] in for await event in accountNotifications.events { @@ -50,21 +57,25 @@ class NavigationManager: Module, EnvironmentAccessible { continue } - logger.debug("Reinitializing navigation path.") - - educationPath = NavigationPath() - medicationsPath = NavigationPath() - heartHealthPath = NavigationPath() - homePath = NavigationPath() - - heartHealthVitalSelection = .symptoms - questionnaireId = nil - showHealthSummary = false - selectedTab = .home + refreshContent() } } } + func refreshContent() { + logger.debug("Reinitializing navigation path.") + + educationPath = NavigationPath() + medicationsPath = NavigationPath() + heartHealthPath = NavigationPath() + homePath = NavigationPath() + + heartHealthVitalSelection = .symptoms + questionnaireId = nil + showHealthSummary = false + selectedTab = .home + } + func execute(_ messageAction: MessageAction) async -> Bool { self.logger.debug("Executing message action: \(messageAction.encodingString ?? "unknown")") diff --git a/ENGAGEHF/Managers/NotificationManager/NotificationManager.swift b/ENGAGEHF/Managers/NotificationManager/NotificationManager.swift index 1438da5c..55536fae 100644 --- a/ENGAGEHF/Managers/NotificationManager/NotificationManager.swift +++ b/ENGAGEHF/Managers/NotificationManager/NotificationManager.swift @@ -14,6 +14,7 @@ import OSLog import Spezi import SpeziAccount import SpeziFoundation +import SpeziNotifications import SpeziViews import SwiftUI import UserNotifications @@ -21,7 +22,7 @@ import UserNotifications @Observable @MainActor -class NotificationManager: Module, NotificationHandler, NotificationTokenHandler, EnvironmentAccessible { +final class NotificationManager: Manager, NotificationHandler, NotificationTokenHandler { private struct NotificationTokenTimeoutError: LocalizedError { var errorDescription: String? { "Remote notification registration timed out." @@ -46,6 +47,9 @@ class NotificationManager: Module, NotificationHandler, NotificationTokenHandler var state: ViewState = .idle + nonisolated init() {} + + func configure() { self.cancellable = NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification).sink { _ in Task { @MainActor in @@ -91,7 +95,27 @@ class NotificationManager: Module, NotificationHandler, NotificationTokenHandler } } - @MainActor + + func refreshContent() { + Task { + do { + if account?.details != nil { + _ = try await self.requestNotificationPermissions() + } else { + _ = try await self.unregisterDeviceToken() + } + } catch { + self.state = .error( + AnyLocalizedError( + error: error, + defaultErrorDescription: "Unable to register for remote notifications." + ) + ) + } + } + } + + func checkNotificationsAuthorized() async { let systemNotificationSettings = await UNUserNotificationCenter.current().notificationSettings() diff --git a/ENGAGEHF/Managers/UserMetaDataManager/UserMetaDataManager.swift b/ENGAGEHF/Managers/UserMetaDataManager/UserMetaDataManager.swift index 502a5164..37d59ae8 100644 --- a/ENGAGEHF/Managers/UserMetaDataManager/UserMetaDataManager.swift +++ b/ENGAGEHF/Managers/UserMetaDataManager/UserMetaDataManager.swift @@ -15,7 +15,7 @@ import SpeziFirebaseAccount @Observable @MainActor -class UserMetaDataManager: Module, EnvironmentAccessible { +final class UserMetaDataManager: Manager { @ObservationIgnored @Dependency(Account.self) private var account: Account? @ObservationIgnored @Dependency(AccountNotifications.self) private var accountNotifications: AccountNotifications? @ObservationIgnored @Application(\.logger) private var logger @@ -27,6 +27,9 @@ class UserMetaDataManager: Module, EnvironmentAccessible { private(set) var organization: Organization? + nonisolated init() {} + + func configure() { if ProcessInfo.processInfo.isPreviewSimulator { return @@ -49,6 +52,12 @@ class UserMetaDataManager: Module, EnvironmentAccessible { } } + + func refreshContent() { + updateOrganizationIfNeeded(id: account?.details?.organization) + } + + private func updateOrganizationIfNeeded(id organizationId: String?) { guard previousOrganizationId != organizationId else { return diff --git a/ENGAGEHF/Managers/VideoManager/VideoManager.swift b/ENGAGEHF/Managers/VideoManager/VideoManager.swift index feb5773d..d26f02b1 100644 --- a/ENGAGEHF/Managers/VideoManager/VideoManager.swift +++ b/ENGAGEHF/Managers/VideoManager/VideoManager.swift @@ -14,7 +14,7 @@ import SpeziAccount @Observable @MainActor -final class VideoManager: Module, EnvironmentAccessible, DefaultInitializable { +final class VideoManager: Manager { @ObservationIgnored @Dependency(Account.self) private var account: Account? @ObservationIgnored @Dependency(AccountNotifications.self) private var accountNotifications: AccountNotifications? @Application(\.logger) @ObservationIgnored private var logger @@ -52,9 +52,14 @@ final class VideoManager: Module, EnvironmentAccessible, DefaultInitializable { } if let account, account.signedIn { - Task { @MainActor in - videoCollections = await getVideoSections() - } + refreshContent() + } + } + + + func refreshContent() { + Task { @MainActor in + videoCollections = await getVideoSections() } } diff --git a/ENGAGEHF/Managers/VitalsManager/VitalsManager.swift b/ENGAGEHF/Managers/VitalsManager/VitalsManager.swift index b375794e..9292a70d 100644 --- a/ENGAGEHF/Managers/VitalsManager/VitalsManager.swift +++ b/ENGAGEHF/Managers/VitalsManager/VitalsManager.swift @@ -23,7 +23,7 @@ import SpeziFirestore /// - Convert FHIR observations to HKQuantitySamples and HKCorrelations @Observable @MainActor -public class VitalsManager: Module, EnvironmentAccessible { +public final class VitalsManager: Manager { @ObservationIgnored @StandardActor private var standard: ENGAGEHFStandard @ObservationIgnored @Dependency(Account.self) private var account: Account? @@ -55,6 +55,9 @@ public class VitalsManager: Module, EnvironmentAccessible { } + nonisolated public init() {} + + /// Call on initial configuration: /// - Add a snapshot listener to the three health data collections public func configure() { @@ -90,6 +93,12 @@ public class VitalsManager: Module, EnvironmentAccessible { } } + + public func refreshContent() { + updateSnapshotListener(for: account?.details) + } + + private func updateSnapshotListener(for details: AccountDetails?) { self.logger.debug("Initializing vitals snapshot listener...") diff --git a/ENGAGEHF/Onboarding/InvitationCodeView.swift b/ENGAGEHF/Onboarding/InvitationCodeView.swift index 608ab8f1..937b72e8 100644 --- a/ENGAGEHF/Onboarding/InvitationCodeView.swift +++ b/ENGAGEHF/Onboarding/InvitationCodeView.swift @@ -20,7 +20,7 @@ struct InvitationCodeView: View { @State private var invitationCode = "" @State private var viewState: ViewState = .idle - + @ValidationState private var validation @@ -52,7 +52,7 @@ struct InvitationCodeView: View { .navigationBarTitleDisplayMode(.large) .navigationTitle(String(localized: "Invitation Code")) } - .navigationBarBackButtonHidden() + .navigationBarBackButtonHidden() } @ViewBuilder private var actionsView: some View { diff --git a/ENGAGEHF/Onboarding/OnboardingFlow.swift b/ENGAGEHF/Onboarding/OnboardingFlow.swift index 97ff536d..c7b6a461 100644 --- a/ENGAGEHF/Onboarding/OnboardingFlow.swift +++ b/ENGAGEHF/Onboarding/OnboardingFlow.swift @@ -33,7 +33,6 @@ struct OnboardingFlow: View { #Preview { OnboardingFlow() .previewWith(standard: ENGAGEHFStandard()) { - OnboardingDataSource() InvitationCodeModule() AccountConfiguration(service: InMemoryAccountService()) } diff --git a/ENGAGEHF/Resources/Assets.xcassets/ENGAGEHFIcon.imageset/Contents.json b/ENGAGEHF/Resources/Assets.xcassets/ENGAGEHFIcon.imageset/Contents.json new file mode 100644 index 00000000..6c22bdab --- /dev/null +++ b/ENGAGEHF/Resources/Assets.xcassets/ENGAGEHFIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "ENGAGEHFIcon.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ENGAGEHF/Resources/Assets.xcassets/HealthSummary.imageset/Contents.json.license b/ENGAGEHF/Resources/Assets.xcassets/ENGAGEHFIcon.imageset/Contents.json.license similarity index 100% rename from ENGAGEHF/Resources/Assets.xcassets/HealthSummary.imageset/Contents.json.license rename to ENGAGEHF/Resources/Assets.xcassets/ENGAGEHFIcon.imageset/Contents.json.license diff --git a/ENGAGEHF/Resources/AppIcon.png b/ENGAGEHF/Resources/Assets.xcassets/ENGAGEHFIcon.imageset/ENGAGEHFIcon.png similarity index 100% rename from ENGAGEHF/Resources/AppIcon.png rename to ENGAGEHF/Resources/Assets.xcassets/ENGAGEHFIcon.imageset/ENGAGEHFIcon.png diff --git a/ENGAGEHF/Resources/AppIcon.png.license b/ENGAGEHF/Resources/Assets.xcassets/ENGAGEHFIcon.imageset/ENGAGEHFIcon.png.license similarity index 100% rename from ENGAGEHF/Resources/AppIcon.png.license rename to ENGAGEHF/Resources/Assets.xcassets/ENGAGEHFIcon.imageset/ENGAGEHFIcon.png.license diff --git a/ENGAGEHF/Resources/Assets.xcassets/HealthSummary.imageset/Contents.json b/ENGAGEHF/Resources/Assets.xcassets/HealthSummary.imageset/Contents.json deleted file mode 100644 index e67efbbf..00000000 --- a/ENGAGEHF/Resources/Assets.xcassets/HealthSummary.imageset/Contents.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "images" : [ - { - "filename" : "HealthSummary.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "idiom" : "universal", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/ENGAGEHF/Resources/Assets.xcassets/HealthSummary.imageset/HealthSummary.png b/ENGAGEHF/Resources/Assets.xcassets/HealthSummary.imageset/HealthSummary.png deleted file mode 100644 index 451773a0..00000000 Binary files a/ENGAGEHF/Resources/Assets.xcassets/HealthSummary.imageset/HealthSummary.png and /dev/null differ diff --git a/ENGAGEHF/Resources/Assets.xcassets/HealthSummary.imageset/HealthSummary.png.license b/ENGAGEHF/Resources/Assets.xcassets/HealthSummary.imageset/HealthSummary.png.license deleted file mode 100644 index 25032d03..00000000 --- a/ENGAGEHF/Resources/Assets.xcassets/HealthSummary.imageset/HealthSummary.png.license +++ /dev/null @@ -1,6 +0,0 @@ - -This source file is part of the ENGAGE-HF based on the Stanford Spezi Template Application project - -SPDX-FileCopyrightText: 2023 Stanford University - -SPDX-License-Identifier: MIT diff --git a/ENGAGEHF/Resources/Localizable.xcstrings b/ENGAGEHF/Resources/Localizable.xcstrings index 7318fc41..b63805ee 100644 --- a/ENGAGEHF/Resources/Localizable.xcstrings +++ b/ENGAGEHF/Resources/Localizable.xcstrings @@ -148,18 +148,6 @@ }, "Cancel" : { - }, - "CLOSE" : { - "comment" : "MARK: General", - "extractionState" : "stale", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Close" - } - } - } }, "Color Key" : { @@ -176,9 +164,6 @@ } } } - }, - "Content Unavailable" : { - }, "Current" : { @@ -203,6 +188,9 @@ }, "Enable Notifications in Settings" : { + }, + "ENGAGE-HF Application Loading Screen" : { + }, "Expansion Button" : { @@ -517,9 +505,6 @@ }, "Resolution Picker" : { - }, - "Retry" : { - }, "Review" : { @@ -655,9 +640,6 @@ }, "The invitation code is invalid or has already been used." : { "comment" : "Invitation Code Invalid" - }, - "The user isn't currently set up correctly. Please try closing the app and opening it back up." : { - }, "There are currently no educational videos available." : { diff --git a/ENGAGEHF/Supporting Files/GoogleService-Info.plist b/ENGAGEHF/Supporting Files/GoogleService-Info.plist index 23c290cf..350cddf0 100644 --- a/ENGAGEHF/Supporting Files/GoogleService-Info.plist +++ b/ENGAGEHF/Supporting Files/GoogleService-Info.plist @@ -31,4 +31,4 @@ GOOGLE_APP_ID 1:123456789012:ios:1234567890123456789012 - + \ No newline at end of file diff --git a/ENGAGEHFUITests/Dashboard/RecentVitalsUITests.swift b/ENGAGEHFUITests/Dashboard/RecentVitalsUITests.swift index 1d095e42..47d6186a 100644 --- a/ENGAGEHFUITests/Dashboard/RecentVitalsUITests.swift +++ b/ENGAGEHFUITests/Dashboard/RecentVitalsUITests.swift @@ -6,9 +6,21 @@ // SPDX-License-Identifier: MIT // +import Foundation import XCTest + final class RecentVitalsUITests: XCTestCase { + private var expectedFormattedMeasurementDate: String { + let expectedDateComponents = DateComponents(year: 2024, month: 6, day: 5, hour: 12, minute: 33, second: 11) + let expectedDate = Calendar.current.date(from: expectedDateComponents) ?? .now + let daylightSavingTimeOffset = TimeZone.current.daylightSavingTimeOffset(for: expectedDate) + let formatter = DateFormatter() + formatter.dateFormat = "M/d/yyyy, h:mm a" + return formatter.string(from: expectedDate.addingTimeInterval(daylightSavingTimeOffset)) + } + + override func setUpWithError() throws { try super.setUpWithError() @@ -53,12 +65,11 @@ final class RecentVitalsUITests: XCTestCase { XCTAssertFalse(app.alerts.element.exists) - // Weight measurement has been successfully saved, and should be represented in the dashboard XCTAssert(app.staticTexts["Recent Vitals"].waitForExistence(timeout: 0.5)) XCTAssert(app.staticTexts["Weight Quantity: \(expectedWeight)"].exists) XCTAssert(app.staticTexts["Weight Unit: \(weightUnit)"].exists) - XCTAssert(app.staticTexts["Weight Date: 6/5/2024, 12:33 PM"].exists) + XCTAssert(app.staticTexts["Weight Date: \(expectedFormattedMeasurementDate)"].exists) app.staticTexts["Weight Quantity: \(expectedWeight)"].tap() XCTAssert(app.staticTexts["Body Weight"].waitForExistence(timeout: 2.0)) @@ -103,7 +114,7 @@ final class RecentVitalsUITests: XCTestCase { let heartRateQuantityText = "Heart Rate Quantity: 62" XCTAssert(app.staticTexts[heartRateQuantityText].exists) XCTAssert(app.staticTexts["Heart Rate Unit: BPM"].exists) - XCTAssert(app.staticTexts["Heart Rate Date: 6/5/2024, 12:33 PM"].exists) + XCTAssert(app.staticTexts["Heart Rate Date: \(expectedFormattedMeasurementDate)"].exists) app.staticTexts[heartRateQuantityText].tap() XCTAssert(app.staticTexts["Heart Rate"].waitForExistence(timeout: 2.0)) @@ -114,7 +125,7 @@ final class RecentVitalsUITests: XCTestCase { let bloodPressureQuantityText = "Blood Pressure Quantity: 103/64" XCTAssert(app.staticTexts[bloodPressureQuantityText].exists) XCTAssert(app.staticTexts["Blood Pressure Unit: mmHg"].exists) - XCTAssert(app.staticTexts["Blood Pressure Date: 6/5/2024, 12:33 PM"].exists) + XCTAssert(app.staticTexts["Blood Pressure Date: \(expectedFormattedMeasurementDate)"].exists) app.staticTexts[bloodPressureQuantityText].tap() XCTAssert(app.staticTexts["Blood Pressure"].waitForExistence(timeout: 2.0)) diff --git a/ENGAGEHFUITests/Education/EducationViewUITests.swift b/ENGAGEHFUITests/Education/EducationViewUITests.swift index fb96972e..acbc315d 100644 --- a/ENGAGEHFUITests/Education/EducationViewUITests.swift +++ b/ENGAGEHFUITests/Education/EducationViewUITests.swift @@ -10,7 +10,8 @@ import XCTest final class EducationViewUITests: XCTestCase { - override func setUpWithError() throws { + @MainActor + override func setUp() async throws { try super.setUpWithError() continueAfterFailure = false @@ -20,6 +21,7 @@ final class EducationViewUITests: XCTestCase { } + @MainActor func testLongDescriptionVideoView() throws { let app = XCUIApplication() diff --git a/ENGAGEHFUITests/HeartHealth/HeartHealthUITests.swift b/ENGAGEHFUITests/HeartHealth/HeartHealthUITests.swift index c7e982e9..3c1cd622 100644 --- a/ENGAGEHFUITests/HeartHealth/HeartHealthUITests.swift +++ b/ENGAGEHFUITests/HeartHealth/HeartHealthUITests.swift @@ -10,7 +10,8 @@ import XCTest final class HeartHealthUITests: XCTestCase { - override func setUpWithError() throws { + @MainActor + override func setUp() async throws { try super.setUpWithError() continueAfterFailure = false @@ -251,7 +252,7 @@ extension XCUIApplication { for vital in measurements { XCTAssert(staticTexts[vital].exists) } - + XCTAssert(buttons["Discard"].exists) XCTAssert(buttons["Save"].exists) diff --git a/ENGAGEHFUITests/Medications/MedicationsUITests.swift b/ENGAGEHFUITests/Medications/MedicationsUITests.swift index d408a4d9..b65fed27 100644 --- a/ENGAGEHFUITests/Medications/MedicationsUITests.swift +++ b/ENGAGEHFUITests/Medications/MedicationsUITests.swift @@ -10,7 +10,8 @@ import XCTest final class MedicationsUITests: XCTestCase { - override func setUpWithError() throws { + @MainActor + override func setUp() async throws { try super.setUpWithError() continueAfterFailure = false @@ -125,7 +126,8 @@ final class MedicationsUITests: XCTestCase { XCTAssert(app.staticTexts["97/103"].waitForExistence(timeout: 0.5), "Multi-ingredient target dose not found.") } - func testMultiScheduleDoseSummary() throws { + @MainActor + func testMultiScheduleDoseSummary() async throws { let app = XCUIApplication() _ = app.staticTexts["Home"].waitForExistence(timeout: 5) diff --git a/fastlane/.gitignore b/fastlane/.gitignore index bb9d865c..72185907 100644 --- a/fastlane/.gitignore +++ b/fastlane/.gitignore @@ -8,4 +8,4 @@ test_output report.xml -screenshots \ No newline at end of file +screenshots diff --git a/fastlane/Appfile b/fastlane/Appfile deleted file mode 100644 index ae1aab13..00000000 --- a/fastlane/Appfile +++ /dev/null @@ -1,13 +0,0 @@ -# -# This source file is part of the ENGAGE-HF based on the Stanford Spezi Template Application project -# -# SPDX-FileCopyrightText: 2023 Stanford University -# -# SPDX-License-Identifier: MIT -# - -# For more information about the Appfile, see: -# https://docs.fastlane.tools/advanced/#appfile - -app_identifier "edu.stanford.bdh.engagehf" # The bundle identifier of your app -apple_id ENV["APPLE_ID"] # Your Apple email address \ No newline at end of file diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 9ae50e34..ec0f520a 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -20,7 +20,7 @@ platform :ios do skip_build: true, derived_data_path: ".derivedData", code_coverage: true, - devices: ["iPhone 15 Pro"], + devices: ["iPhone 16 Plus"], disable_slide_to_type: false, concurrent_workers: 1, max_concurrent_simulators: 1, @@ -33,8 +33,45 @@ platform :ios do ) end - desc "CodeQL" - lane :codeql do + desc "Screenshots" + lane :screenshots do + run_tests( + destination: "generic/platform=iOS Simulator", + configuration: "Debug", + derived_data_path: ".derivedData", + xcargs: [ + "-skipPackagePluginValidation", + "-skipMacroValidation" + ], + build_for_testing: true + ) + + snapshot( + test_without_building: true, + derived_data_path: ".derivedData", + devices: [ + "iPhone 16 Plus", + "iPad Pro 13-inch (M4)" + ], + languages: [ + "en-US", + ], + scheme: "ENGAGEHF", + output_directory: "./fastlane/screenshots", + clear_previous_screenshots: true, + concurrent_simulators: false, + stop_after_first_error: true, + skip_open_summary: true + ) + + # Workaround for https://github.com/fastlane/fastlane/issues/21759 and + Dir.glob("./screenshots/**/iPhone 16 Plus-*.png").each do |file| + sh("sips --resampleHeightWidth 2796 1290 '#{file}'") + end + end + + desc "Build app" + lane :build do build_app( skip_archive: true, skip_codesigning: true, @@ -46,8 +83,16 @@ platform :ios do ) end - desc "Build app" - lane :build do + desc "Archive app" + lane :archive do |options| + appidentifier = options[:appidentifier] || "edu.stanford.bdh.engagehf" + provisioningProfile = options[:provisioningProfile] || "Stanford BDHG ENGAGE-HF" + + update_app_identifier( + plist_path: "ENGAGEHF/Supporting Files/Info.plist", + app_identifier: appidentifier + ) + build_app( derived_data_path: ".derivedData", xcargs: [ @@ -56,7 +101,7 @@ platform :ios do ], export_options: { provisioningProfiles: { - "edu.stanford.bdh.engagehf" => "Stanford BDHG ENGAGE-HF" + appidentifier => provisioningProfile } } ) @@ -72,25 +117,46 @@ platform :ios do ) end - desc "Publish a beta release to internal TestFlight testers" - lane :beta do + desc "Publish a release to TestFlight or the App Store depending on the environment" + lane :deploy do |options| + environment = options[:environment] || "staging" + appidentifier = options[:appidentifier] || "edu.stanford.bdh.engagehf" + provisioningProfile = options[:provisioningProfile] || "Stanford BDHG ENGAGE-HF" + signin increment_build_number( { build_number: latest_testflight_build_number + 1 } ) - build - commit = last_git_commit - upload_to_testflight( - distribute_external: true, - groups: [ - "External Testers" - ], - submit_beta_review: true, - notify_external_testers: true, - expire_previous_builds: true, - changelog: commit[:message] + archive( + appidentifier: appidentifier, + provisioningProfile: provisioningProfile, ) + commit = last_git_commit + + if environment == "production" + deliver( + app_identifier: appidentifier, + username: ENV["APPLE_ID"], + submit_for_review: true, + force: true, + reject_if_possible: true, + automatic_release: true, + precheck_include_in_app_purchases: false, + ) + else + upload_to_testflight( + app_identifier: appidentifier, + apple_id: ENV["APPLE_ID"], + distribute_external: true, + groups: [ + "External Testers" + ], + submit_beta_review: true, + notify_external_testers: true, + changelog: commit[:message] + ) + end end end diff --git a/fastlane/SnapshotHelper.swift b/fastlane/SnapshotHelper.swift new file mode 100644 index 00000000..7e5e26e9 --- /dev/null +++ b/fastlane/SnapshotHelper.swift @@ -0,0 +1,317 @@ +// +// This source file is part of the ENGAGE-HF based on the Stanford Spezi Template Application project +// Generated from `fastlane snapshot init` and originally authored by Felix Krause on 10/8/15 +// +// SPDX-FileCopyrightText: 2023 Stanford University +// +// SPDX-License-Identifier: MIT +// + +// ----------------------------------------------------- +// IMPORTANT: When modifying this file, make sure to +// increment the version number at the very +// bottom of the file to notify users about +// the new SnapshotHelper.swift +// ----------------------------------------------------- + +// periphery:ignore:all + +import Foundation +import XCTest + +@MainActor +func setupSnapshot(_ app: XCUIApplication, waitForAnimations: Bool = true) { + Snapshot.setupSnapshot(app, waitForAnimations: waitForAnimations) +} + +@MainActor +func snapshot(_ name: String, waitForLoadingIndicator: Bool) { + if waitForLoadingIndicator { + Snapshot.snapshot(name) + } else { + Snapshot.snapshot(name, timeWaitingForIdle: 0) + } +} + +/// - Parameters: +/// - name: The name of the snapshot +/// - timeout: Amount of seconds to wait until the network loading indicator disappears. Pass `0` if you don't want to wait. +@MainActor +func snapshot(_ name: String, timeWaitingForIdle timeout: TimeInterval = 20) { + Snapshot.snapshot(name, timeWaitingForIdle: timeout) +} + +enum SnapshotError: Error, CustomDebugStringConvertible { + case cannotFindSimulatorHomeDirectory + case cannotRunOnPhysicalDevice + + var debugDescription: String { + switch self { + case .cannotFindSimulatorHomeDirectory: + return "Couldn't find simulator home location. Please, check SIMULATOR_HOST_HOME env variable." + case .cannotRunOnPhysicalDevice: + return "Can't use Snapshot on a physical device." + } + } +} + +@objcMembers +@MainActor +open class Snapshot: NSObject { + static var app: XCUIApplication? + static var waitForAnimations = true + static var cacheDirectory: URL? + static var screenshotsDirectory: URL? { + return cacheDirectory?.appendingPathComponent("screenshots", isDirectory: true) + } + static var deviceLanguage = "" + static var currentLocale = "" + + open class func setupSnapshot(_ app: XCUIApplication, waitForAnimations: Bool = true) { + + Snapshot.app = app + Snapshot.waitForAnimations = waitForAnimations + + do { + let cacheDir = try getCacheDirectory() + Snapshot.cacheDirectory = cacheDir + setLanguage(app) + setLocale(app) + setLaunchArguments(app) + } catch { + NSLog(error.localizedDescription) + } + } + + class func setLanguage(_ app: XCUIApplication) { + guard let cacheDirectory = self.cacheDirectory else { + NSLog("CacheDirectory is not set - probably running on a physical device?") + return + } + + let path = cacheDirectory.appendingPathComponent("language.txt") + + do { + let trimCharacterSet = CharacterSet.whitespacesAndNewlines + deviceLanguage = try String(contentsOf: path, encoding: .utf8).trimmingCharacters(in: trimCharacterSet) + app.launchArguments += ["-AppleLanguages", "(\(deviceLanguage))"] + } catch { + NSLog("Couldn't detect/set language...") + } + } + + class func setLocale(_ app: XCUIApplication) { + guard let cacheDirectory = self.cacheDirectory else { + NSLog("CacheDirectory is not set - probably running on a physical device?") + return + } + + let path = cacheDirectory.appendingPathComponent("locale.txt") + + do { + let trimCharacterSet = CharacterSet.whitespacesAndNewlines + currentLocale = try String(contentsOf: path, encoding: .utf8).trimmingCharacters(in: trimCharacterSet) + } catch { + NSLog("Couldn't detect/set locale...") + } + + if currentLocale.isEmpty && !deviceLanguage.isEmpty { + currentLocale = Locale(identifier: deviceLanguage).identifier + } + + if !currentLocale.isEmpty { + app.launchArguments += ["-AppleLocale", "\"\(currentLocale)\""] + } + } + + class func setLaunchArguments(_ app: XCUIApplication) { + guard let cacheDirectory = self.cacheDirectory else { + NSLog("CacheDirectory is not set - probably running on a physical device?") + return + } + + let path = cacheDirectory.appendingPathComponent("snapshot-launch_arguments.txt") + app.launchArguments += ["-FASTLANE_SNAPSHOT", "YES", "-ui_testing"] + + do { + let launchArguments = try String(contentsOf: path, encoding: String.Encoding.utf8) + let regex = try NSRegularExpression(pattern: "(\\\".+?\\\"|\\S+)", options: []) + let matches = regex.matches(in: launchArguments, options: [], range: NSRange(location: 0, length: launchArguments.count)) + let results = matches.map { result -> String in + (launchArguments as NSString).substring(with: result.range) + } + app.launchArguments += results + } catch { + NSLog("Couldn't detect/set launch_arguments...") + } + } + + open class func snapshot(_ name: String, timeWaitingForIdle timeout: TimeInterval = 20) { + if timeout > 0 { + waitForLoadingIndicatorToDisappear(within: timeout) + } + + NSLog("snapshot: \(name)") // more information about this, check out https://docs.fastlane.tools/actions/snapshot/#how-does-it-work + + if Snapshot.waitForAnimations { + sleep(1) // Waiting for the animation to be finished (kind of) + } + + #if os(OSX) + guard let app = self.app else { + NSLog("XCUIApplication is not set. Please call setupSnapshot(app) before snapshot().") + return + } + + app.typeKey(XCUIKeyboardKeySecondaryFn, modifierFlags: []) + #else + + guard self.app != nil else { + NSLog("XCUIApplication is not set. Please call setupSnapshot(app) before snapshot().") + return + } + + let screenshot = XCUIScreen.main.screenshot() + #if os(iOS) && !targetEnvironment(macCatalyst) + let image = XCUIDevice.shared.orientation.isLandscape ? fixLandscapeOrientation(image: screenshot.image) : screenshot.image + #else + let image = screenshot.image + #endif + + guard var simulator = ProcessInfo().environment["SIMULATOR_DEVICE_NAME"], let screenshotsDir = screenshotsDirectory else { return } + + do { + // The simulator name contains "Clone X of " inside the screenshot file when running parallelized UI Tests on concurrent devices + let regex = try NSRegularExpression(pattern: "Clone [0-9]+ of ") + let range = NSRange(location: 0, length: simulator.count) + simulator = regex.stringByReplacingMatches(in: simulator, range: range, withTemplate: "") + + let path = screenshotsDir.appendingPathComponent("\(simulator)-\(name).png") + #if swift(<5.0) + try UIImagePNGRepresentation(image)?.write(to: path, options: .atomic) + #else + try image.pngData()?.write(to: path, options: .atomic) + #endif + } catch let error { + NSLog("Problem writing screenshot: \(name) to \(screenshotsDir)/\(simulator)-\(name).png") + NSLog(error.localizedDescription) + } + #endif + } + + class func fixLandscapeOrientation(image: UIImage) -> UIImage { + #if os(watchOS) + return image + #else + if #available(iOS 10.0, *) { + let format = UIGraphicsImageRendererFormat() + format.scale = image.scale + let renderer = UIGraphicsImageRenderer(size: image.size, format: format) + return renderer.image { context in + image.draw(in: CGRect(x: 0, y: 0, width: image.size.width, height: image.size.height)) + } + } else { + return image + } + #endif + } + + class func waitForLoadingIndicatorToDisappear(within timeout: TimeInterval) { + #if os(tvOS) + return + #endif + + guard let app = self.app else { + NSLog("XCUIApplication is not set. Please call setupSnapshot(app) before snapshot().") + return + } + + let networkLoadingIndicator = app.otherElements.deviceStatusBars.networkLoadingIndicators.element + let networkLoadingIndicatorDisappeared = XCTNSPredicateExpectation(predicate: NSPredicate(format: "exists == false"), object: networkLoadingIndicator) + _ = XCTWaiter.wait(for: [networkLoadingIndicatorDisappeared], timeout: timeout) + } + + class func getCacheDirectory() throws -> URL { + let cachePath = "Library/Caches/tools.fastlane" + // on OSX config is stored in /Users//Library + // and on iOS/tvOS/WatchOS it's in simulator's home dir + #if os(OSX) + let homeDir = URL(fileURLWithPath: NSHomeDirectory()) + return homeDir.appendingPathComponent(cachePath) + #elseif arch(i386) || arch(x86_64) || arch(arm64) + guard let simulatorHostHome = ProcessInfo().environment["SIMULATOR_HOST_HOME"] else { + throw SnapshotError.cannotFindSimulatorHomeDirectory + } + let homeDir = URL(fileURLWithPath: simulatorHostHome) + return homeDir.appendingPathComponent(cachePath) + #else + throw SnapshotError.cannotRunOnPhysicalDevice + #endif + } +} + +private extension XCUIElementAttributes { + var isNetworkLoadingIndicator: Bool { + if hasAllowListedIdentifier { return false } + + let hasOldLoadingIndicatorSize = frame.size == CGSize(width: 10, height: 20) + let hasNewLoadingIndicatorSize = frame.size.width.isBetween(46, and: 47) && frame.size.height.isBetween(2, and: 3) + + return hasOldLoadingIndicatorSize || hasNewLoadingIndicatorSize + } + + var hasAllowListedIdentifier: Bool { + let allowListedIdentifiers = ["GeofenceLocationTrackingOn", "StandardLocationTrackingOn"] + + return allowListedIdentifiers.contains(identifier) + } + + func isStatusBar(_ deviceWidth: CGFloat) -> Bool { + if elementType == .statusBar { return true } + guard frame.origin == .zero else { return false } + + let oldStatusBarSize = CGSize(width: deviceWidth, height: 20) + let newStatusBarSize = CGSize(width: deviceWidth, height: 44) + + return [oldStatusBarSize, newStatusBarSize].contains(frame.size) + } +} + +private extension XCUIElementQuery { + var networkLoadingIndicators: XCUIElementQuery { + let isNetworkLoadingIndicator = NSPredicate { (evaluatedObject, _) in + guard let element = evaluatedObject as? XCUIElementAttributes else { return false } + + return element.isNetworkLoadingIndicator + } + + return self.containing(isNetworkLoadingIndicator) + } + + @MainActor + var deviceStatusBars: XCUIElementQuery { + guard let app = Snapshot.app else { + fatalError("XCUIApplication is not set. Please call setupSnapshot(app) before snapshot().") + } + + let deviceWidth = app.windows.firstMatch.frame.width + + let isStatusBar = NSPredicate { (evaluatedObject, _) in + guard let element = evaluatedObject as? XCUIElementAttributes else { return false } + + return element.isStatusBar(deviceWidth) + } + + return self.containing(isStatusBar) + } +} + +private extension CGFloat { + func isBetween(_ numberA: CGFloat, and numberB: CGFloat) -> Bool { + return numberA...numberB ~= self + } +} + +// Please don't remove the lines below +// They are used to detect outdated configuration files +// SnapshotHelperVersion [1.30]