From a2776cfaa38213816ff5dae151c44fcd6a82bd98 Mon Sep 17 00:00:00 2001 From: Paul Schmiedmayer Date: Tue, 29 Oct 2024 10:26:06 -0700 Subject: [PATCH 01/33] Improve Deployment Setup --- .github/workflows/beta-deployment.yml | 66 +++++- ENGAGEHF.xcodeproj/project.pbxproj | 4 + fastlane/Appfile | 13 -- fastlane/Fastfile | 97 ++++++-- fastlane/SnapshotHelper.swift | 315 ++++++++++++++++++++++++++ 5 files changed, 459 insertions(+), 36 deletions(-) delete mode 100644 fastlane/Appfile create mode 100644 fastlane/SnapshotHelper.swift diff --git a/.github/workflows/beta-deployment.yml b/.github/workflows/beta-deployment.yml index 15d053f0..1b9bdbec 100644 --- a/.github/workflows/beta-deployment.yml +++ b/.github/workflows/beta-deployment.yml @@ -6,30 +6,86 @@ # SPDX-License-Identifier: MIT # -name: Beta Deployment +name: Deployment on: push: 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: deployment + cancel-in-progress: false jobs: + determineenvironment: + name: Determine Environment + runs-on: ubuntu-latest + outputs: + environment: ${{ steps.set-env.outputs.environment }} + steps: + - name: Checkout code + uses: actions/checkout@v4 + - name: Determine Environment + id: set-env + run: | + if [[ -z "${{ inputs.environment }}" ]]; then + echo "environment=staging" >> $GITHUB_OUTPUT + else + echo "environment=${{ inputs.environment }}" >> $GITHUB_OUTPUT + 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: + FIREBASE_PROJECT_ID: ${{ vars.FIREBASE_PROJECT_ID }} + steps: + - run: | + echo "Injecting Environment Variables In Deployment Workflow: ${{ vars.FIREBASE_PROJECT_ID }}" buildandtest: name: Build and Test + needs: determineenvironment uses: ./.github/workflows/build-and-test.yml permissions: contents: read + actions: read + security-events: write 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.FIREBASE_PROJECT_ID }} + fastlanelane: deploy environment:"${{ needs.determineenvironment.outputs.environment }} appidentifier:"${{ needs.determineenvironment.outputs.environment }}" + secrets: inherit \ No newline at end of file diff --git a/ENGAGEHF.xcodeproj/project.pbxproj b/ENGAGEHF.xcodeproj/project.pbxproj index 0f06be49..a59092f8 100644 --- a/ENGAGEHF.xcodeproj/project.pbxproj +++ b/ENGAGEHF.xcodeproj/project.pbxproj @@ -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 */; }; @@ -267,6 +268,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 = ""; }; @@ -761,6 +763,7 @@ 4D84027B2C4EE61C00817495 /* HelperFunctions */ = { isa = PBXGroup; children = ( + 2FF8F0512CC69015002C757A /* SnapshotHelper.swift */, 4D8402792C4EE56000817495 /* XCUIApplication+GoTo.swift */, 4D8402832C4F11CB00817495 /* XCUIApplication+DeleteMeasurements.swift */, ); @@ -1539,6 +1542,7 @@ 4D8402842C4F11CB00817495 /* XCUIApplication+DeleteMeasurements.swift in Sources */, 4D8402772C4EE52000817495 /* AddMeasurementUITests.swift in Sources */, 4D8AD2E12C4B5D1600CB4F3E /* HeartHealthUITests.swift in Sources */, + 2FF8F0522CC69015002C757A /* SnapshotHelper.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; 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..3dc0f7dd 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -21,7 +21,9 @@ platform :ios do derived_data_path: ".derivedData", code_coverage: true, devices: ["iPhone 15 Pro"], - disable_slide_to_type: false, + force_quit_simulator: true, + reset_simulator: true, + prelaunch_simulator: false, concurrent_workers: 1, max_concurrent_simulators: 1, result_bundle: true, @@ -33,8 +35,50 @@ 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 Pro", + "iPhone SE (3rd generation)" + ], + 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 Pro-*.png").each do |file| + sh("sips --resampleHeightWidth 2778 1284 '#{file}'") + end + + # Scale to 1242 x 2208 as there are no iOS 18 Simulators supporting this screen size. + Dir.glob("./screenshots/**/iPhone SE (3rd generation)-*.png").each do |file| + sh("sips --resampleHeightWidth 2208 1242 '#{file}'") + end + end + + desc "Build app" + lane :build do build_app( skip_archive: true, skip_codesigning: true, @@ -46,8 +90,8 @@ platform :ios do ) end - desc "Build app" - lane :build do + desc "Archive app" + lane :archive do build_app( derived_data_path: ".derivedData", xcargs: [ @@ -72,25 +116,42 @@ 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" + signin increment_build_number( { build_number: latest_testflight_build_number + 1 } ) - build + archive 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] - ) + + 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..fba88dcd --- /dev/null +++ b/fastlane/SnapshotHelper.swift @@ -0,0 +1,315 @@ +// +// 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 +// ----------------------------------------------------- + +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 let error { + 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] From 84d886a91dec09ea58e92a3948c2134f9f8472d3 Mon Sep 17 00:00:00 2001 From: nriedman <108841122+nriedman@users.noreply.github.com> Date: Tue, 5 Nov 2024 13:23:34 -0800 Subject: [PATCH 02/33] Implicitly capture error without casting --- fastlane/SnapshotHelper.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastlane/SnapshotHelper.swift b/fastlane/SnapshotHelper.swift index fba88dcd..ea89d8e9 100644 --- a/fastlane/SnapshotHelper.swift +++ b/fastlane/SnapshotHelper.swift @@ -76,7 +76,7 @@ open class Snapshot: NSObject { setLanguage(app) setLocale(app) setLaunchArguments(app) - } catch let error { + } catch { NSLog(error.localizedDescription) } } From 383593f7983ba23ff82fdb683ab04455e36803ba Mon Sep 17 00:00:00 2001 From: Paul Schmiedmayer Date: Wed, 13 Nov 2024 00:26:22 -0800 Subject: [PATCH 03/33] Update Dependencies --- ENGAGEHF.xcodeproj/project.pbxproj | 47 +++++++++---- .../xcshareddata/swiftpm/Package.resolved | 69 +++++++++++-------- ENGAGEHF/Account/AccountSetupHeader.swift | 4 +- ENGAGEHF/ENGAGEHFStandard.swift | 35 +--------- .../NotificationManager.swift | 1 + 5 files changed, 75 insertions(+), 81 deletions(-) diff --git a/ENGAGEHF.xcodeproj/project.pbxproj b/ENGAGEHF.xcodeproj/project.pbxproj index a59092f8..147f8e08 100644 --- a/ENGAGEHF.xcodeproj/project.pbxproj +++ b/ENGAGEHF.xcodeproj/project.pbxproj @@ -203,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 */; }; @@ -452,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 */, @@ -1203,6 +1205,7 @@ A977DD552C25E2DA00A2A8E5 /* SpeziDevicesUI */, A977DD5C2C26279E00A2A8E5 /* SpeziBluetoothServices */, 9B0723AA2C8F6AE700D901E5 /* FirebaseMessaging */, + 65D39ABC2CE497630040BF71 /* SpeziNotifications */, ); productName = ENGAGEHF; productReference = 653A254D283387FE005D4D48 /* ENGAGEHF.app */; @@ -1302,6 +1305,7 @@ 4D49AAFC2BC9D50400C77310 /* XCRemoteSwiftPackageReference "SpeziBluetooth" */, A9A0BF012C13121D00B8F3F3 /* XCRemoteSwiftPackageReference "SpeziNetworking" */, A9623C022C17A3F500189BA1 /* XCRemoteSwiftPackageReference "SpeziDevices" */, + 65D39ABB2CE497630040BF71 /* XCRemoteSwiftPackageReference "SpeziNotifications" */, ); productRefGroup = 653A254E283387FE005D4D48 /* Products */; projectDirPath = ""; @@ -2040,7 +2044,7 @@ repositoryURL = "https://github.com/StanfordSpezi/Spezi"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 1.7.0; + minimumVersion = 1.8.0; }; }; 2F66D20D2BB723180010D555 /* XCRemoteSwiftPackageReference "SwiftLint" */ = { @@ -2048,7 +2052,7 @@ repositoryURL = "https://github.com/realm/SwiftLint.git"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 0.55.0; + minimumVersion = 0.57.0; }; }; 2FB099B42A875E2B00B20952 /* XCRemoteSwiftPackageReference "HealthKitOnFHIR" */ = { @@ -2064,7 +2068,7 @@ repositoryURL = "https://github.com/StanfordSpezi/SpeziAccount.git"; requirement = { kind = upToNextMajorVersion; - minimumVersion = "2.0.0-beta.8"; + minimumVersion = 2.1.0; }; }; 2FE5DC6529EDD894004B9AB4 /* XCRemoteSwiftPackageReference "SpeziContact" */ = { @@ -2072,7 +2076,7 @@ repositoryURL = "https://github.com/StanfordSpezi/SpeziContact.git"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 1.0.0; + minimumVersion = 1.0.2; }; }; 2FE5DC7029EDD8D3004B9AB4 /* XCRemoteSwiftPackageReference "SpeziHealthKit" */ = { @@ -2088,7 +2092,7 @@ repositoryURL = "https://github.com/StanfordSpezi/SpeziFirebase.git"; requirement = { kind = upToNextMajorVersion; - minimumVersion = "2.0.0-beta.4"; + minimumVersion = 2.0.0; }; }; 2FE5DC8229EDD934004B9AB4 /* XCRemoteSwiftPackageReference "SpeziQuestionnaire" */ = { @@ -2096,7 +2100,7 @@ repositoryURL = "https://github.com/StanfordSpezi/SpeziQuestionnaire.git"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 1.0.0; + minimumVersion = 1.2.3; }; }; 2FE5DC8829EDD972004B9AB4 /* XCRemoteSwiftPackageReference "SpeziStorage" */ = { @@ -2104,7 +2108,7 @@ repositoryURL = "https://github.com/StanfordSpezi/SpeziStorage.git"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 1.0.0; + minimumVersion = 1.2.1; }; }; 2FE5DC8D29EDD980004B9AB4 /* XCRemoteSwiftPackageReference "SpeziViews" */ = { @@ -2112,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" */ = { @@ -2120,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" */ = { @@ -2128,7 +2132,7 @@ repositoryURL = "https://github.com/StanfordBDHG/XCTestExtensions.git"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 1.0.0; + minimumVersion = 1.1.0; }; }; 2FE5DC9A29EDD9EF004B9AB4 /* XCRemoteSwiftPackageReference "XCTHealthKit" */ = { @@ -2144,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" */ = { @@ -2160,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" */ = { @@ -2168,7 +2180,7 @@ repositoryURL = "https://github.com/StanfordSpezi/SpeziOnboarding"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 1.0.0; + minimumVersion = 1.2.2; }; }; A9623C022C17A3F500189BA1 /* XCRemoteSwiftPackageReference "SpeziDevices" */ = { @@ -2176,7 +2188,7 @@ repositoryURL = "https://github.com/StanfordSpezi/SpeziDevices.git"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 1.3.0; + minimumVersion = 1.4.1; }; }; A9A0BF012C13121D00B8F3F3 /* XCRemoteSwiftPackageReference "SpeziNetworking" */ = { @@ -2184,7 +2196,7 @@ repositoryURL = "https://github.com/StanfordSpezi/SpeziNetworking"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 2.1.0; + minimumVersion = 2.2.0; }; }; /* End XCRemoteSwiftPackageReference section */ @@ -2310,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..d8dd77e2 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" : "ee970020efb1d24dddac538ebb3e07effa694b71d4d92b624c7c7de473d81fa2", "pins" : [ { "identity" : "abseil-cpp-binary", @@ -60,8 +60,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/firebase/firebase-ios-sdk.git", "state" : { - "revision" : "1fc52ab0e172e7c5a961f975a76c2611f4f22852", - "version" : "11.2.0" + "revision" : "dbdfdc44bee8b8e4eaa5ec27eb12b9338f3f2bc1", + "version" : "11.5.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" } }, { @@ -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" : "0e4dcc7d3284b439b17fae621c5c6e73d9213696", + "version" : "2.1.0" } }, { @@ -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" : "7c6829624884f6f1d700e0316b2580b39d3b0c5f", + "version" : "2.0.0" } }, { @@ -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,8 +312,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/StanfordSpezi/SpeziViews.git", "state" : { - "revision" : "427f4f3a7acb0e00ea11c4c3ca7b60e36d2557a0", - "version" : "1.6.0" + "revision" : "f87514406bb57ae67d0040eec5454fff55104143", + "version" : "1.7.0" } }, { @@ -364,10 +373,10 @@ { "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" : "515f79b522918f83483068d99c68daeb5116342d", + "version" : "600.0.0-prerelease-2024-08-14" } }, { @@ -384,8 +393,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/realm/SwiftLint.git", "state" : { - "revision" : "b515723b16eba33f15c4677ee65f3fef2ce8c255", - "version" : "0.55.1" + "revision" : "168fb98ed1f3e343d703ecceaf518b6cf565207b", + "version" : "0.57.0" } }, { @@ -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" } }, { diff --git a/ENGAGEHF/Account/AccountSetupHeader.swift b/ENGAGEHF/Account/AccountSetupHeader.swift index 51e181e9..69c4d3c8 100644 --- a/ENGAGEHF/Account/AccountSetupHeader.swift +++ b/ENGAGEHF/Account/AccountSetupHeader.swift @@ -12,7 +12,7 @@ import SwiftUI struct AccountSetupHeader: View { @Environment(Account.self) private var account - @Environment(\._accountSetupState) private var setupState + @Environment(\.accountSetupState) private var setupState var body: some View { @@ -24,7 +24,7 @@ struct AccountSetupHeader: View { .padding(.top, 30) Text("ACCOUNT_SUBTITLE") .padding(.bottom, 8) - if account.signedIn, case .generic = setupState { + if account.signedIn, case .presentingExistingAccount = setupState { Text("ACCOUNT_SIGNED_IN_DESCRIPTION") } else { Text("ACCOUNT_SETUP_DESCRIPTION") 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/Managers/NotificationManager/NotificationManager.swift b/ENGAGEHF/Managers/NotificationManager/NotificationManager.swift index 1438da5c..53712e2e 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 From 8567d1e57d6e2961a552a595e19eeeb263b3dfc5 Mon Sep 17 00:00:00 2001 From: Paul Schmiedmayer Date: Wed, 13 Nov 2024 00:29:08 -0800 Subject: [PATCH 04/33] Update Swiftlint Exclude for Autogenerated Files --- .swiftlint.yml | 1 + 1 file changed, 1 insertion(+) 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 From 04f23e3983d54e3d6c3a365eb9818f8dc1bfcc38 Mon Sep 17 00:00:00 2001 From: Paul Schmiedmayer Date: Wed, 13 Nov 2024 00:33:15 -0800 Subject: [PATCH 05/33] Update --- ENGAGEHF/Resources/Localizable.xcstrings | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/ENGAGEHF/Resources/Localizable.xcstrings b/ENGAGEHF/Resources/Localizable.xcstrings index 7318fc41..3f9f208a 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" : { From 1e6d58c7d5709ac32f2b18ce9358ef880d987210 Mon Sep 17 00:00:00 2001 From: Paul Schmiedmayer Date: Wed, 13 Nov 2024 01:01:42 -0800 Subject: [PATCH 06/33] Update Setup --- .github/workflows/beta-deployment.yml | 24 ++++++++------- .github/workflows/build-and-test.yml | 19 +++--------- .github/workflows/static-analysis.yml | 42 +++++++++++++++++++++++++++ fastlane/Fastfile | 30 +++++++++++-------- 4 files changed, 77 insertions(+), 38 deletions(-) create mode 100644 .github/workflows/static-analysis.yml diff --git a/.github/workflows/beta-deployment.yml b/.github/workflows/beta-deployment.yml index 1b9bdbec..f931c149 100644 --- a/.github/workflows/beta-deployment.yml +++ b/.github/workflows/beta-deployment.yml @@ -6,7 +6,7 @@ # SPDX-License-Identifier: MIT # -name: Deployment +name: Beta Deployment on: push: @@ -34,7 +34,7 @@ on: default: staging concurrency: - group: deployment + group: Beta-Deployment cancel-in-progress: false jobs: @@ -42,18 +42,17 @@ jobs: name: Determine Environment runs-on: ubuntu-latest outputs: - environment: ${{ steps.set-env.outputs.environment }} + environment: ${{ steps.determineenvironment.outputs.environment }} steps: - name: Checkout code uses: actions/checkout@v4 - name: Determine Environment - id: set-env + id: determineenvironment run: | if [[ -z "${{ inputs.environment }}" ]]; then echo "environment=staging" >> $GITHUB_OUTPUT else echo "environment=${{ inputs.environment }}" >> $GITHUB_OUTPUT - echo "environment=${{ inputs.environment }}" >> $GITHUB_OUTPUT fi vars: name: Inject Environment Variables In Deployment Workflow @@ -61,18 +60,21 @@ jobs: runs-on: ubuntu-latest environment: ${{ needs.determineenvironment.outputs.environment }} outputs: - FIREBASE_PROJECT_ID: ${{ vars.FIREBASE_PROJECT_ID }} + firebaseprojectid: ${{ vars.FIREBASE_PROJECT_ID }} + appidentifier: ${{ vars.APP_IDENTIFIER }} + provisioningProfileName: ${{ vars.PROVISIONING_PROFILE_NAME }} steps: - run: | - echo "Injecting Environment Variables In Deployment Workflow: ${{ vars.FIREBASE_PROJECT_ID }}" + 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 - actions: read - security-events: write secrets: inherit iosapptestflightdeployment: name: iOS App TestFlight Deployment @@ -86,6 +88,6 @@ jobs: googleserviceinfoplistpath: 'PAWS/Supporting Files/GoogleService-Info.plist' setupsigning: true setupfirebaseemulator: true - firebaseemulatorimport: ./firebase --project ${{ needs.vars.outputs.FIREBASE_PROJECT_ID }} - fastlanelane: deploy environment:"${{ needs.determineenvironment.outputs.environment }} appidentifier:"${{ needs.determineenvironment.outputs.environment }}" + 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..18bc6b60 --- /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: StanfordSpezi/.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/fastlane/Fastfile b/fastlane/Fastfile index 3dc0f7dd..8533c63b 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -52,8 +52,7 @@ platform :ios do test_without_building: true, derived_data_path: ".derivedData", devices: [ - "iPhone 16 Pro", - "iPhone SE (3rd generation)" + "iPhone 16 Pro Max" ], languages: [ "en-US", @@ -67,13 +66,8 @@ platform :ios do ) # Workaround for https://github.com/fastlane/fastlane/issues/21759 and - Dir.glob("./screenshots/**/iPhone 16 Pro-*.png").each do |file| - sh("sips --resampleHeightWidth 2778 1284 '#{file}'") - end - - # Scale to 1242 x 2208 as there are no iOS 18 Simulators supporting this screen size. - Dir.glob("./screenshots/**/iPhone SE (3rd generation)-*.png").each do |file| - sh("sips --resampleHeightWidth 2208 1242 '#{file}'") + Dir.glob("./screenshots/**/iPhone 16 Pro Max-*.png").each do |file| + sh("sips --resampleHeightWidth 2796 1290 '#{file}'") end end @@ -91,7 +85,15 @@ platform :ios do end desc "Archive app" - lane :archive do + 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", + app_identifier: appidentifier + ) + build_app( derived_data_path: ".derivedData", xcargs: [ @@ -100,7 +102,7 @@ platform :ios do ], export_options: { provisioningProfiles: { - "edu.stanford.bdh.engagehf" => "Stanford BDHG ENGAGE-HF" + appidentifier => provisioningProfile } } ) @@ -120,6 +122,7 @@ platform :ios do 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( @@ -127,7 +130,10 @@ platform :ios do build_number: latest_testflight_build_number + 1 } ) - archive + archive( + appidentifier: appidentifier, + provisioningProfile: provisioningProfile, + ) commit = last_git_commit if environment == "production" From 91b414e22147166306e198e920be1130d4260ec9 Mon Sep 17 00:00:00 2001 From: Paul Schmiedmayer Date: Wed, 13 Nov 2024 01:03:56 -0800 Subject: [PATCH 07/33] Use BDHG Action --- .github/workflows/static-analysis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml index 18bc6b60..00426e44 100644 --- a/.github/workflows/static-analysis.yml +++ b/.github/workflows/static-analysis.yml @@ -30,7 +30,7 @@ jobs: contents: read periphery: name: Periphery - uses: StanfordSpezi/.github/.github/workflows/periphery.yml@v2 + uses: StanfordBDHG/.github/.github/workflows/periphery.yml@v2 permissions: contents: read with: From 7b7d47e31e64ee0e4215a852bf5ea4ff0adf6f18 Mon Sep 17 00:00:00 2001 From: Paul Schmiedmayer Date: Tue, 19 Nov 2024 07:18:13 -0800 Subject: [PATCH 08/33] Update --- .../xcshareddata/swiftpm/Package.resolved | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/ENGAGEHF.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ENGAGEHF.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index d8dd77e2..8e51f352 100644 --- a/ENGAGEHF.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/ENGAGEHF.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "ee970020efb1d24dddac538ebb3e07effa694b71d4d92b624c7c7de473d81fa2", + "originHash" : "2f8235f304c1dd7bcfc68f692f99b4029356877d29f596e8787777ad4f475cb3", "pins" : [ { "identity" : "abseil-cpp-binary", @@ -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" } }, { @@ -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" } }, { @@ -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" } }, { @@ -337,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" } }, { @@ -348,8 +348,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-nio.git", "state" : { - "revision" : "9746cf80e29edfef2a39924a66731249223f42a3", - "version" : "2.72.0" + "revision" : "914081701062b11e3bb9e21accc379822621995e", + "version" : "2.76.1" } }, { @@ -357,8 +357,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/FelixHerrmann/swift-package-list", "state" : { - "revision" : "60b77bea44f5b2f7369aecfcd993f7672e09602a", - "version" : "4.3.0" + "revision" : "26732b1cf7e422cb330a1e24420394752a14b059", + "version" : "4.4.1" } }, { @@ -366,8 +366,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-protobuf.git", "state" : { - "revision" : "edb6ed4919f7756157fe02f2552b7e3850a538e5", - "version" : "1.28.1" + "revision" : "ebc7251dd5b37f627c93698e4374084d98409633", + "version" : "1.28.2" } }, { @@ -384,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" } }, { From 777ce9dc1833499f5133b57c84835c3ea495597b Mon Sep 17 00:00:00 2001 From: Paul Schmiedmayer Date: Tue, 19 Nov 2024 09:48:23 -0800 Subject: [PATCH 09/33] Fix Crash --- ENGAGEHF/Account/InvitationCodeModule.swift | 13 +++++----- ENGAGEHF/ContentView.swift | 8 +++--- ENGAGEHF/ENGAGEHFDelegate.swift | 1 - .../NavigationManager/NavigationManager.swift | 5 +++- ENGAGEHF/Onboarding/OnboardingFlow.swift | 1 - .../Supporting Files/GoogleService-Info.plist | 26 ++++++++----------- 6 files changed, 25 insertions(+), 29 deletions(-) diff --git a/ENGAGEHF/Account/InvitationCodeModule.swift b/ENGAGEHF/Account/InvitationCodeModule.swift index 9c9f36e3..292f11ec 100644 --- a/ENGAGEHF/Account/InvitationCodeModule.swift +++ b/ENGAGEHF/Account/InvitationCodeModule.swift @@ -22,7 +22,7 @@ class InvitationCodeModule: Module, EnvironmentAccessible { @Dependency(FirebaseAccountService.self) private var accountService: FirebaseAccountService? func configure() { - if FeatureFlags.useFirebaseEmulator { + if FeatureFlags.useFirebaseEmulator && !FeatureFlags.disableFirebase { let firestoreHost = FeatureFlags.useCustomFirestoreHost ? FirestoreSettings.customHost : FirestoreSettings.defaultHost Functions.functions().useEmulator(withHost: firestoreHost, port: 5001) } @@ -68,12 +68,11 @@ class InvitationCodeModule: Module, EnvironmentAccessible { @MainActor func setupTestEnvironment(invitationCode: String) async throws { - guard let account else { - preconditionFailure("Account feature must be enabled to support `setupTestEnvironment` flag!") - } - - guard let accountService else { - preconditionFailure("The Firebase Account Service is required to be configured when setting up the test environment!") + guard let account, let accountService else { + guard FeatureFlags.disableFirebase else { + preconditionFailure("The Firebase Account Service is required to be configured when setting up the test environment!") + } + return } let email = "test@engage.stanford.edu" diff --git a/ENGAGEHF/ContentView.swift b/ENGAGEHF/ContentView.swift index 9b3f2023..91c99905 100644 --- a/ENGAGEHF/ContentView.swift +++ b/ENGAGEHF/ContentView.swift @@ -23,19 +23,19 @@ struct ContentView: View { } @AppStorage(StorageKeys.onboardingFlowComplete) private var completedOnboardingFlow = false - @Environment(Account.self) private var account: Account + @Environment(Account.self) private var account: Account? @State private var sheetContent: SheetContent? private var expectedSheetContent: SheetContent? { guard FeatureFlags.skipOnboarding || completedOnboardingFlow else { return .onboarding } - guard FeatureFlags.disableFirebase || account.signedIn else { + guard FeatureFlags.disableFirebase || account?.signedIn ?? false else { return .auth } guard FeatureFlags.disableFirebase - || account.details?.isIncomplete ?? true - || account.details?.invitationCode != nil else { + || account?.details?.isIncomplete ?? true + || account?.details?.invitationCode != nil else { return .auth } return nil diff --git a/ENGAGEHF/ENGAGEHFDelegate.swift b/ENGAGEHF/ENGAGEHFDelegate.swift index 081bebf5..2dffbcb1 100644 --- a/ENGAGEHF/ENGAGEHFDelegate.swift +++ b/ENGAGEHF/ENGAGEHFDelegate.swift @@ -81,7 +81,6 @@ class ENGAGEHFDelegate: SpeziAppDelegate { MedicationsManager() VideoManager() - OnboardingDataSource() InvitationCodeModule() ConfigureTipKit() diff --git a/ENGAGEHF/Managers/NavigationManager/NavigationManager.swift b/ENGAGEHF/Managers/NavigationManager/NavigationManager.swift index d21d6420..839c612e 100644 --- a/ENGAGEHF/Managers/NavigationManager/NavigationManager.swift +++ b/ENGAGEHF/Managers/NavigationManager/NavigationManager.swift @@ -39,7 +39,10 @@ class NavigationManager: Module, EnvironmentAccessible { // 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 { 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/Supporting Files/GoogleService-Info.plist b/ENGAGEHF/Supporting Files/GoogleService-Info.plist index 23c290cf..4f102716 100644 --- a/ENGAGEHF/Supporting Files/GoogleService-Info.plist +++ b/ENGAGEHF/Supporting Files/GoogleService-Info.plist @@ -2,33 +2,29 @@ - CLIENT_ID - CLIENT_ID - REVERSED_CLIENT_ID - REVERSED_CLIENT_ID API_KEY - API_KEY89012345678901234567890123456789 + AIzaSyDtSimbzLPmb7v-n2u9612y-PdXrFHNg9o GCM_SENDER_ID - GCM_SENDER_ID + 672231779872 PLIST_VERSION 1 BUNDLE_ID edu.stanford.bdh.engagehf PROJECT_ID - stanford-bdhg-engage-hf + som-rit-phi-enghf2-dev STORAGE_BUCKET - STORAGE_BUCKET + som-rit-phi-enghf2-dev.appspot.com IS_ADS_ENABLED - + IS_ANALYTICS_ENABLED - + IS_APPINVITE_ENABLED - + IS_GCM_ENABLED - + IS_SIGNIN_ENABLED - + GOOGLE_APP_ID - 1:123456789012:ios:1234567890123456789012 + 1:672231779872:ios:3519454ff7a793a6295645 - + \ No newline at end of file From 8d3cc7eb0d083e9453fdeedee1c6a9567a1d4cf2 Mon Sep 17 00:00:00 2001 From: Paul Schmiedmayer Date: Thu, 21 Nov 2024 11:38:36 -0800 Subject: [PATCH 10/33] Fix --- .../Supporting Files/GoogleService-Info.plist | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/ENGAGEHF/Supporting Files/GoogleService-Info.plist b/ENGAGEHF/Supporting Files/GoogleService-Info.plist index 4f102716..350cddf0 100644 --- a/ENGAGEHF/Supporting Files/GoogleService-Info.plist +++ b/ENGAGEHF/Supporting Files/GoogleService-Info.plist @@ -2,29 +2,33 @@ + CLIENT_ID + CLIENT_ID + REVERSED_CLIENT_ID + REVERSED_CLIENT_ID API_KEY - AIzaSyDtSimbzLPmb7v-n2u9612y-PdXrFHNg9o + API_KEY89012345678901234567890123456789 GCM_SENDER_ID - 672231779872 + GCM_SENDER_ID PLIST_VERSION 1 BUNDLE_ID edu.stanford.bdh.engagehf PROJECT_ID - som-rit-phi-enghf2-dev + stanford-bdhg-engage-hf STORAGE_BUCKET - som-rit-phi-enghf2-dev.appspot.com + STORAGE_BUCKET IS_ADS_ENABLED - + IS_ANALYTICS_ENABLED - + IS_APPINVITE_ENABLED - + IS_GCM_ENABLED - + IS_SIGNIN_ENABLED - + GOOGLE_APP_ID - 1:672231779872:ios:3519454ff7a793a6295645 + 1:123456789012:ios:1234567890123456789012 \ No newline at end of file From 808a39579860c9512c14297a0f9995cd9c7d2bfc Mon Sep 17 00:00:00 2001 From: Paul Schmiedmayer Date: Thu, 21 Nov 2024 11:39:56 -0800 Subject: [PATCH 11/33] Update GitIgnore --- fastlane/.gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/fastlane/.gitignore b/fastlane/.gitignore index bb9d865c..62285e62 100644 --- a/fastlane/.gitignore +++ b/fastlane/.gitignore @@ -8,4 +8,5 @@ test_output report.xml -screenshots \ No newline at end of file +screenshots +ENGAGE/Supporting Files/GoogleService-Info.plist \ No newline at end of file From 7a1e68ccb1279835562c8c4ead170b3a9644de1d Mon Sep 17 00:00:00 2001 From: Paul Schmiedmayer Date: Thu, 21 Nov 2024 11:41:40 -0800 Subject: [PATCH 12/33] Update GitIgnore --- fastlane/.gitignore | 1 - 1 file changed, 1 deletion(-) diff --git a/fastlane/.gitignore b/fastlane/.gitignore index 62285e62..72185907 100644 --- a/fastlane/.gitignore +++ b/fastlane/.gitignore @@ -9,4 +9,3 @@ test_output report.xml screenshots -ENGAGE/Supporting Files/GoogleService-Info.plist \ No newline at end of file From b5bbbe0203e05b73d3b86fe12e0016fcdfcd0a4e Mon Sep 17 00:00:00 2001 From: Paul Schmiedmayer Date: Thu, 21 Nov 2024 11:48:15 -0800 Subject: [PATCH 13/33] Update Fastlane Setup --- ENGAGEHF.xcodeproj/project.pbxproj | 6 +++--- fastlane/Fastfile | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/ENGAGEHF.xcodeproj/project.pbxproj b/ENGAGEHF.xcodeproj/project.pbxproj index 147f8e08..fce2564b 100644 --- a/ENGAGEHF.xcodeproj/project.pbxproj +++ b/ENGAGEHF.xcodeproj/project.pbxproj @@ -1896,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"; @@ -1917,10 +1917,10 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = edu.stanford.bdh.engagehf; + 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; diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 8533c63b..76b7ebef 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 Pro"], force_quit_simulator: true, reset_simulator: true, prelaunch_simulator: false, @@ -52,7 +52,7 @@ platform :ios do test_without_building: true, derived_data_path: ".derivedData", devices: [ - "iPhone 16 Pro Max" + "iPhone 16 Pro" ], languages: [ "en-US", @@ -66,8 +66,8 @@ platform :ios do ) # Workaround for https://github.com/fastlane/fastlane/issues/21759 and - Dir.glob("./screenshots/**/iPhone 16 Pro Max-*.png").each do |file| - sh("sips --resampleHeightWidth 2796 1290 '#{file}'") + Dir.glob("./screenshots/**/iPhone 16 Pro-*.png").each do |file| + sh("sips --resampleHeightWidth 2778 1284 '#{file}'") end end From b5dd7165831c1e9031b571cecc5159be9617b3eb Mon Sep 17 00:00:00 2001 From: Paul Schmiedmayer Date: Thu, 21 Nov 2024 11:49:24 -0800 Subject: [PATCH 14/33] Update Fastlane Setup --- ENGAGEHF.xcodeproj/project.pbxproj | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ENGAGEHF.xcodeproj/project.pbxproj b/ENGAGEHF.xcodeproj/project.pbxproj index fce2564b..147f8e08 100644 --- a/ENGAGEHF.xcodeproj/project.pbxproj +++ b/ENGAGEHF.xcodeproj/project.pbxproj @@ -1896,7 +1896,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_TEAM = ""; - "DEVELOPMENT_TEAM[sdk=iphoneos*]" = CQRZ4E7K9U; + "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 637867499T; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = "ENGAGEHF/Supporting Files/Info.plist"; @@ -1917,10 +1917,10 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = edu.stanford.hrtex; + PRODUCT_BUNDLE_IDENTIFIER = edu.stanford.bdh.engagehf; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; - "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "ENGAGE-HF - Biodesign Digital Health"; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "Stanford BDHG ENGAGE-HF"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; From f4d49f306ad3f73d534ac74c85b9315ce3eef61d Mon Sep 17 00:00:00 2001 From: Paul Schmiedmayer Date: Thu, 21 Nov 2024 15:24:03 -0800 Subject: [PATCH 15/33] Update Setup --- .gitignore | 2 +- ENGAGEHF.xcodeproj/project.pbxproj | 6 +- ENGAGEHF/Onboarding/InvitationCodeView.swift | 55 +++++++++++++++---- ENGAGEHF/Resources/Localizable.xcstrings | 6 ++ .../Education/EducationViewUITests.swift | 7 ++- .../HeartHealth/HeartHealthUITests.swift | 10 +++- .../Medications/MedicationsUITests.swift | 9 ++- fastlane/Fastfile | 3 +- 8 files changed, 78 insertions(+), 20 deletions(-) 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/ENGAGEHF.xcodeproj/project.pbxproj b/ENGAGEHF.xcodeproj/project.pbxproj index 147f8e08..87f4a8ea 100644 --- a/ENGAGEHF.xcodeproj/project.pbxproj +++ b/ENGAGEHF.xcodeproj/project.pbxproj @@ -1676,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 = ""; @@ -1872,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 = ""; @@ -1916,7 +1916,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 = ""; diff --git a/ENGAGEHF/Onboarding/InvitationCodeView.swift b/ENGAGEHF/Onboarding/InvitationCodeView.swift index 608ab8f1..f920eff9 100644 --- a/ENGAGEHF/Onboarding/InvitationCodeView.swift +++ b/ENGAGEHF/Onboarding/InvitationCodeView.swift @@ -16,10 +16,12 @@ import SwiftUI struct InvitationCodeView: View { @Environment(OnboardingNavigationPath.self) private var onboardingNavigationPath @Environment(InvitationCodeModule.self) private var invitationCodeModule + @Environment(AccountNotifications.self) private var accountNotifications @Environment(Account.self) private var account @State private var invitationCode = "" - @State private var viewState: ViewState = .idle + @State private var viewState: ViewState = .processing + @State private var accountNotificationsTask: Task? @ValidationState private var validation @@ -36,15 +38,26 @@ struct InvitationCodeView: View { var body: some View { ScrollView { VStack(spacing: 32) { - invitationCodeHeader - Divider() - Grid(horizontalSpacing: 16, verticalSpacing: 16) { - invitationCodeView + if viewState == .processing { + ContentUnavailableView { + Label("ENGAGE-HF", systemImage: "person.crop.circle") + } description: { + Text("Preparing your Account") + Spacer() + ProgressView() + } + .padding(.vertical, 64) + } else { + invitationCodeHeader + Divider() + Grid(horizontalSpacing: 16, verticalSpacing: 16) { + invitationCodeView + } + .padding(.top, -8) + .padding(.bottom, -12) + Divider() + actionsView } - .padding(.top, -8) - .padding(.bottom, -12) - Divider() - actionsView } .padding(.horizontal) .padding(.bottom) @@ -52,7 +65,29 @@ struct InvitationCodeView: View { .navigationBarTitleDisplayMode(.large) .navigationTitle(String(localized: "Invitation Code")) } - .navigationBarBackButtonHidden() + .navigationBarBackButtonHidden() + .task { + try? await Task.sleep(for: .seconds(0.5)) + + guard account.details?.invitationCode == nil else { + onboardingNavigationPath.removeLast() + onboardingNavigationPath.nextStep() + return + } + + accountNotificationsTask = Task.detached { @MainActor in + for await event in accountNotifications.events where event.accountDetails?.invitationCode != nil { + onboardingNavigationPath.removeLast() + onboardingNavigationPath.nextStep() + } + } + + viewState = .idle + } + .onDisappear { + accountNotificationsTask?.cancel() + accountNotificationsTask = nil + } } @ViewBuilder private var actionsView: some View { diff --git a/ENGAGEHF/Resources/Localizable.xcstrings b/ENGAGEHF/Resources/Localizable.xcstrings index 3f9f208a..9fb3a371 100644 --- a/ENGAGEHF/Resources/Localizable.xcstrings +++ b/ENGAGEHF/Resources/Localizable.xcstrings @@ -191,6 +191,9 @@ }, "Enable Notifications in Settings" : { + }, + "ENGAGE-HF" : { + }, "Expansion Button" : { @@ -475,6 +478,9 @@ }, "Please enter your invitation code to join the ENGAGE-HF study." : { + }, + "Preparing your Account" : { + }, "Process timed out." : { diff --git a/ENGAGEHFUITests/Education/EducationViewUITests.swift b/ENGAGEHFUITests/Education/EducationViewUITests.swift index fb96972e..caf8f1dd 100644 --- a/ENGAGEHFUITests/Education/EducationViewUITests.swift +++ b/ENGAGEHFUITests/Education/EducationViewUITests.swift @@ -10,16 +10,19 @@ import XCTest final class EducationViewUITests: XCTestCase { - override func setUpWithError() throws { + @MainActor + override func setUp() async throws { try super.setUpWithError() continueAfterFailure = false let app = XCUIApplication() app.launchArguments = ["--assumeOnboardingComplete", "--setupTestEnvironment", "--useFirebaseEmulator", "--setupTestVideos"] app.launch() + setupSnapshot(app) } + @MainActor func testLongDescriptionVideoView() throws { let app = XCUIApplication() @@ -29,6 +32,7 @@ final class EducationViewUITests: XCTestCase { let thumbnailOverlay = app.staticTexts["Thumbnail Overlay Title: Long Description"] XCTAssert(thumbnailOverlay.waitForExistence(timeout: 0.5)) + snapshot("5Education") thumbnailOverlay.tap() sleep(2) @@ -66,6 +70,7 @@ final class EducationViewUITests: XCTestCase { let descriptionText = scrollableText.staticTexts["Scrollable Text"] XCTAssert(descriptionText.exists) XCTAssertEqual(descriptionText.label, expectedDescription) + snapshot("EducationVideo") } diff --git a/ENGAGEHFUITests/HeartHealth/HeartHealthUITests.swift b/ENGAGEHFUITests/HeartHealth/HeartHealthUITests.swift index c7e982e9..6a0829d1 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 @@ -22,6 +23,7 @@ final class HeartHealthUITests: XCTestCase { "--useFirebaseEmulator" ] app.launch() + setupSnapshot(app) } func testSymptomScores() throws { @@ -251,9 +253,13 @@ extension XCUIApplication { for vital in measurements { XCTAssert(staticTexts[vital].exists) } - + XCTAssert(buttons["Discard"].exists) XCTAssert(buttons["Save"].exists) + + if displayName == "Blood Pressure" { + ENGAGEHFUITests.snapshot("3BloodPressureMeasurement") + } buttons["Save"].tap() sleep(1) diff --git a/ENGAGEHFUITests/Medications/MedicationsUITests.swift b/ENGAGEHFUITests/Medications/MedicationsUITests.swift index d408a4d9..1e9d6c31 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 @@ -18,6 +19,7 @@ final class MedicationsUITests: XCTestCase { let app = XCUIApplication() app.launchArguments = ["--skipOnboarding", "--setupTestEnvironment", "--useFirebaseEmulator", "--setupTestMedications"] app.launch() + setupSnapshot(app) } @@ -125,7 +127,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) @@ -145,6 +148,8 @@ final class MedicationsUITests: XCTestCase { XCTAssert(app.staticTexts["5"].waitForExistence(timeout: 0.5), "Second component of current dose not found.") XCTAssert(app.staticTexts["mg"].firstMatch.waitForExistence(timeout: 0.5), "Units not found.") XCTAssert(app.staticTexts["daily"].firstMatch.waitForExistence(timeout: 0.5), "\"Daily\" quantifier not found.") + + snapshot("4Medications") } func testFrequencyStyling() throws { diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 76b7ebef..09780296 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -52,7 +52,8 @@ platform :ios do test_without_building: true, derived_data_path: ".derivedData", devices: [ - "iPhone 16 Pro" + "iPhone 16 Pro", + "iPad Pro 13-inch (M4)" ], languages: [ "en-US", From ec3969dc1e741e51acefa6c6ad454522631f7e19 Mon Sep 17 00:00:00 2001 From: Paul Schmiedmayer Date: Fri, 22 Nov 2024 13:47:51 -0800 Subject: [PATCH 16/33] Improve Onboarding Flow --- .../xcshareddata/xcschemes/ENGAGEHF.xcscheme | 8 +- ENGAGEHF/ContentView.swift | 76 +++++-------------- ENGAGEHF/ENGAGEHFDelegate.swift | 7 +- ENGAGEHF/Onboarding/InvitationCodeView.swift | 9 ++- ENGAGEHF/Resources/Localizable.xcstrings | 9 --- fastlane/Fastfile | 8 +- 6 files changed, 40 insertions(+), 77 deletions(-) diff --git a/ENGAGEHF.xcodeproj/xcshareddata/xcschemes/ENGAGEHF.xcscheme b/ENGAGEHF.xcodeproj/xcshareddata/xcschemes/ENGAGEHF.xcscheme index eca0e38c..f51da6dc 100644 --- a/ENGAGEHF.xcodeproj/xcshareddata/xcschemes/ENGAGEHF.xcscheme +++ b/ENGAGEHF.xcodeproj/xcshareddata/xcschemes/ENGAGEHF.xcscheme @@ -83,7 +83,7 @@ + isEnabled = "NO"> + isEnabled = "NO"> + isEnabled = "NO"> + isEnabled = "NO"> diff --git a/ENGAGEHF/ContentView.swift b/ENGAGEHF/ContentView.swift index 91c99905..4d07caae 100644 --- a/ENGAGEHF/ContentView.swift +++ b/ENGAGEHF/ContentView.swift @@ -11,6 +11,7 @@ import SpeziOnboarding import SpeziViews import SwiftUI + @MainActor struct ContentView: View { private enum SheetContent: String, Identifiable { @@ -22,9 +23,10 @@ struct ContentView: View { } } + @AppStorage(StorageKeys.onboardingFlowComplete) private var completedOnboardingFlow = false @Environment(Account.self) private var account: Account? - @State private var sheetContent: SheetContent? + private var expectedSheetContent: SheetContent? { guard FeatureFlags.skipOnboarding || completedOnboardingFlow else { @@ -42,63 +44,23 @@ struct ContentView: View { } var body: some View { - ZStack { - if sheetContent != nil { - VStack { - ContentUnavailableView( - "Content Unavailable", - systemImage: "person.fill.questionmark", - description: Text("The user isn't currently set up correctly. Please try closing the app and opening it back up.") - ) - Button("Retry") { - sheetContent = nil - updateSheetContent() + HomeView() + .accountRequired( + accountSetupIsComplete: { _ in + expectedSheetContent == nil + }, + setupSheet: { + switch expectedSheetContent { + case .onboarding: + OnboardingFlow() + .interactiveDismissDisabled(true) + case .auth: + AuthFlow() + .interactiveDismissDisabled(true) + case .none: + EmptyView() } } - } else { - HomeView() - } - } - .onChange(of: expectedSheetContent, initial: true) { - Task { @MainActor in - // Delaying this update to ensure that animations are done - // and the AccountSheet is actually dismissed already before continuing. - try? await Task.sleep(for: .seconds(0.5)) - updateSheetContent() - } - } - .sheet(isPresented: $sheetContent.exists()) { - Group { - switch sheetContent { - case .onboarding: - OnboardingFlow() - case .auth: - AuthFlow() - case .none: - EmptyView() - } - } - .interactiveDismissDisabled(true) - } - } - - @MainActor - private func updateSheetContent() { - sheetContent = expectedSheetContent - print("updated sheetContent: ", sheetContent?.rawValue ?? "nil") - } -} - -extension Binding { - fileprivate func exists() -> 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 - } - } + ) } } diff --git a/ENGAGEHF/ENGAGEHFDelegate.swift b/ENGAGEHF/ENGAGEHFDelegate.swift index 2dffbcb1..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, diff --git a/ENGAGEHF/Onboarding/InvitationCodeView.swift b/ENGAGEHF/Onboarding/InvitationCodeView.swift index f920eff9..8aacffed 100644 --- a/ENGAGEHF/Onboarding/InvitationCodeView.swift +++ b/ENGAGEHF/Onboarding/InvitationCodeView.swift @@ -67,7 +67,7 @@ struct InvitationCodeView: View { } .navigationBarBackButtonHidden() .task { - try? await Task.sleep(for: .seconds(0.5)) + try? await Task.sleep(for: .seconds(1)) guard account.details?.invitationCode == nil else { onboardingNavigationPath.removeLast() @@ -77,8 +77,13 @@ struct InvitationCodeView: View { accountNotificationsTask = Task.detached { @MainActor in for await event in accountNotifications.events where event.accountDetails?.invitationCode != nil { + guard (accountNotificationsTask?.isCancelled ?? true) == false else { + return + } + onboardingNavigationPath.removeLast() onboardingNavigationPath.nextStep() + accountNotificationsTask?.cancel() } } @@ -86,7 +91,6 @@ struct InvitationCodeView: View { } .onDisappear { accountNotificationsTask?.cancel() - accountNotificationsTask = nil } } @@ -98,6 +102,7 @@ struct InvitationCodeView: View { return } do { + accountNotificationsTask?.cancel() try await invitationCodeModule.verifyOnboardingCode(invitationCode) onboardingNavigationPath.nextStep() } catch { diff --git a/ENGAGEHF/Resources/Localizable.xcstrings b/ENGAGEHF/Resources/Localizable.xcstrings index 9fb3a371..fee09e04 100644 --- a/ENGAGEHF/Resources/Localizable.xcstrings +++ b/ENGAGEHF/Resources/Localizable.xcstrings @@ -164,9 +164,6 @@ } } } - }, - "Content Unavailable" : { - }, "Current" : { @@ -511,9 +508,6 @@ }, "Resolution Picker" : { - }, - "Retry" : { - }, "Review" : { @@ -649,9 +643,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/fastlane/Fastfile b/fastlane/Fastfile index 09780296..9798be66 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 16 Pro"], + devices: ["iPhone 16 Plus"], force_quit_simulator: true, reset_simulator: true, prelaunch_simulator: false, @@ -52,7 +52,7 @@ platform :ios do test_without_building: true, derived_data_path: ".derivedData", devices: [ - "iPhone 16 Pro", + "iPhone 16 Plus", "iPad Pro 13-inch (M4)" ], languages: [ @@ -67,8 +67,8 @@ platform :ios do ) # Workaround for https://github.com/fastlane/fastlane/issues/21759 and - Dir.glob("./screenshots/**/iPhone 16 Pro-*.png").each do |file| - sh("sips --resampleHeightWidth 2778 1284 '#{file}'") + Dir.glob("./screenshots/**/iPhone 16 Plus-*.png").each do |file| + sh("sips --resampleHeightWidth 2796 1290 '#{file}'") end end From d2055ba7579a2787966266ceaabbd49b96b443b0 Mon Sep 17 00:00:00 2001 From: Paul Schmiedmayer Date: Fri, 22 Nov 2024 14:20:10 -0800 Subject: [PATCH 17/33] Update Inital Screen --- ENGAGEHF/ContentView.swift | 13 ++++++++++++- .../HealthSummary.imageset/Contents.json | 11 +---------- ENGAGEHF/Resources/Localizable.xcstrings | 3 +++ 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/ENGAGEHF/ContentView.swift b/ENGAGEHF/ContentView.swift index 4d07caae..fa762c97 100644 --- a/ENGAGEHF/ContentView.swift +++ b/ENGAGEHF/ContentView.swift @@ -44,7 +44,18 @@ struct ContentView: View { } var body: some View { - HomeView() + ZStack(alignment: .center) { + if account?.signedIn ?? false { + HomeView() + } else { + Image(.healthSummary) + .resizable() + .scaledToFit() + .frame(width: 128, height: 128) + .clipShape(RoundedRectangle(cornerRadius: 32)) + .accessibilityLabel("ENGAGE-HF Application Loading Screen") + } + } .accountRequired( accountSetupIsComplete: { _ in expectedSheetContent == nil diff --git a/ENGAGEHF/Resources/Assets.xcassets/HealthSummary.imageset/Contents.json b/ENGAGEHF/Resources/Assets.xcassets/HealthSummary.imageset/Contents.json index e67efbbf..c823d899 100644 --- a/ENGAGEHF/Resources/Assets.xcassets/HealthSummary.imageset/Contents.json +++ b/ENGAGEHF/Resources/Assets.xcassets/HealthSummary.imageset/Contents.json @@ -2,16 +2,7 @@ "images" : [ { "filename" : "HealthSummary.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "idiom" : "universal", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" + "idiom" : "universal" } ], "info" : { diff --git a/ENGAGEHF/Resources/Localizable.xcstrings b/ENGAGEHF/Resources/Localizable.xcstrings index fee09e04..65eb1407 100644 --- a/ENGAGEHF/Resources/Localizable.xcstrings +++ b/ENGAGEHF/Resources/Localizable.xcstrings @@ -191,6 +191,9 @@ }, "ENGAGE-HF" : { + }, + "ENGAGE-HF Application Loading Screen" : { + }, "Expansion Button" : { From 9784905b97f4ef19bb0bb65bb1f7b386cf05c26b Mon Sep 17 00:00:00 2001 From: Paul Schmiedmayer Date: Fri, 22 Nov 2024 14:44:04 -0800 Subject: [PATCH 18/33] Update Assets --- .github/workflows/static-analysis.yml | 14 +++++++------- .periphery.yml | 15 +++++++++++++++ ENGAGEHF.xcodeproj/project.pbxproj | 10 +++------- ENGAGEHF/ContentView.swift | 2 +- ENGAGEHF/HealthSummary/HealthSummaryView.swift | 2 +- .../Contents.json | 2 +- .../Contents.json.license | 0 .../ENGAGEHFIcon.imageset/ENGAGEHFIcon.png} | Bin .../ENGAGEHFIcon.png.license} | 0 .../HealthSummary.imageset/HealthSummary.png | Bin 103467 -> 0 bytes .../HealthSummary.png.license | 6 ------ fastlane/Fastfile | 2 +- fastlane/SnapshotHelper.swift | 2 ++ 13 files changed, 31 insertions(+), 24 deletions(-) create mode 100644 .periphery.yml rename ENGAGEHF/Resources/Assets.xcassets/{HealthSummary.imageset => ENGAGEHFIcon.imageset}/Contents.json (75%) rename ENGAGEHF/Resources/Assets.xcassets/{HealthSummary.imageset => ENGAGEHFIcon.imageset}/Contents.json.license (100%) rename ENGAGEHF/Resources/{AppIcon.png => Assets.xcassets/ENGAGEHFIcon.imageset/ENGAGEHFIcon.png} (100%) rename ENGAGEHF/Resources/{AppIcon.png.license => Assets.xcassets/ENGAGEHFIcon.imageset/ENGAGEHFIcon.png.license} (100%) delete mode 100644 ENGAGEHF/Resources/Assets.xcassets/HealthSummary.imageset/HealthSummary.png delete mode 100644 ENGAGEHF/Resources/Assets.xcassets/HealthSummary.imageset/HealthSummary.png.license diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml index 00426e44..3f644d9c 100644 --- a/.github/workflows/static-analysis.yml +++ b/.github/workflows/static-analysis.yml @@ -28,13 +28,13 @@ jobs: 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"]' + # 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 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/ENGAGEHF.xcodeproj/project.pbxproj b/ENGAGEHF.xcodeproj/project.pbxproj index 87f4a8ea..c3c61ec4 100644 --- a/ENGAGEHF.xcodeproj/project.pbxproj +++ b/ENGAGEHF.xcodeproj/project.pbxproj @@ -23,7 +23,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 */; }; @@ -259,7 +258,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 = ""; }; @@ -536,7 +534,6 @@ 2FE5DC2D29EDD792004B9AB4 /* Resources */ = { isa = PBXGroup; children = ( - 2FE5DC2A29EDD78D004B9AB4 /* AppIcon.png */, 653A255428338800005D4D48 /* Assets.xcassets */, 2FA0BFEC2ACC977500E0EF83 /* Localizable.xcstrings */, 2FE5DC5529EDD811004B9AB4 /* SocialSupportQuestionnaire.json */, @@ -1323,7 +1320,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 */, @@ -1896,7 +1892,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"; @@ -1917,10 +1913,10 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 2.0; - PRODUCT_BUNDLE_IDENTIFIER = edu.stanford.bdh.engagehf; + 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; diff --git a/ENGAGEHF/ContentView.swift b/ENGAGEHF/ContentView.swift index fa762c97..904e2859 100644 --- a/ENGAGEHF/ContentView.swift +++ b/ENGAGEHF/ContentView.swift @@ -48,7 +48,7 @@ struct ContentView: View { if account?.signedIn ?? false { HomeView() } else { - Image(.healthSummary) + Image(.engagehfIcon) .resizable() .scaledToFit() .frame(width: 128, height: 128) 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/Resources/Assets.xcassets/HealthSummary.imageset/Contents.json b/ENGAGEHF/Resources/Assets.xcassets/ENGAGEHFIcon.imageset/Contents.json similarity index 75% rename from ENGAGEHF/Resources/Assets.xcassets/HealthSummary.imageset/Contents.json rename to ENGAGEHF/Resources/Assets.xcassets/ENGAGEHFIcon.imageset/Contents.json index c823d899..6c22bdab 100644 --- a/ENGAGEHF/Resources/Assets.xcassets/HealthSummary.imageset/Contents.json +++ b/ENGAGEHF/Resources/Assets.xcassets/ENGAGEHFIcon.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "HealthSummary.png", + "filename" : "ENGAGEHFIcon.png", "idiom" : "universal" } ], 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/HealthSummary.png b/ENGAGEHF/Resources/Assets.xcassets/HealthSummary.imageset/HealthSummary.png deleted file mode 100644 index 451773a00e51613bd7be66f69ad3c481fa8ce9ce..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 103467 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 eCm>r8;Y3EagY?(RC Date: Sun, 24 Nov 2024 15:31:27 +0100 Subject: [PATCH 19/33] Remove workaround again --- ENGAGEHF/Onboarding/InvitationCodeView.swift | 59 ++++---------------- 1 file changed, 10 insertions(+), 49 deletions(-) diff --git a/ENGAGEHF/Onboarding/InvitationCodeView.swift b/ENGAGEHF/Onboarding/InvitationCodeView.swift index 8aacffed..72d1b20c 100644 --- a/ENGAGEHF/Onboarding/InvitationCodeView.swift +++ b/ENGAGEHF/Onboarding/InvitationCodeView.swift @@ -20,9 +20,8 @@ struct InvitationCodeView: View { @Environment(Account.self) private var account @State private var invitationCode = "" - @State private var viewState: ViewState = .processing - @State private var accountNotificationsTask: Task? - + @State private var viewState: ViewState = .idle + @ValidationState private var validation @@ -38,26 +37,15 @@ struct InvitationCodeView: View { var body: some View { ScrollView { VStack(spacing: 32) { - if viewState == .processing { - ContentUnavailableView { - Label("ENGAGE-HF", systemImage: "person.crop.circle") - } description: { - Text("Preparing your Account") - Spacer() - ProgressView() - } - .padding(.vertical, 64) - } else { - invitationCodeHeader - Divider() - Grid(horizontalSpacing: 16, verticalSpacing: 16) { - invitationCodeView - } - .padding(.top, -8) - .padding(.bottom, -12) - Divider() - actionsView + invitationCodeHeader + Divider() + Grid(horizontalSpacing: 16, verticalSpacing: 16) { + invitationCodeView } + .padding(.top, -8) + .padding(.bottom, -12) + Divider() + actionsView } .padding(.horizontal) .padding(.bottom) @@ -66,32 +54,6 @@ struct InvitationCodeView: View { .navigationTitle(String(localized: "Invitation Code")) } .navigationBarBackButtonHidden() - .task { - try? await Task.sleep(for: .seconds(1)) - - guard account.details?.invitationCode == nil else { - onboardingNavigationPath.removeLast() - onboardingNavigationPath.nextStep() - return - } - - accountNotificationsTask = Task.detached { @MainActor in - for await event in accountNotifications.events where event.accountDetails?.invitationCode != nil { - guard (accountNotificationsTask?.isCancelled ?? true) == false else { - return - } - - onboardingNavigationPath.removeLast() - onboardingNavigationPath.nextStep() - accountNotificationsTask?.cancel() - } - } - - viewState = .idle - } - .onDisappear { - accountNotificationsTask?.cancel() - } } @ViewBuilder private var actionsView: some View { @@ -102,7 +64,6 @@ struct InvitationCodeView: View { return } do { - accountNotificationsTask?.cancel() try await invitationCodeModule.verifyOnboardingCode(invitationCode) onboardingNavigationPath.nextStep() } catch { From d394dc0b0be618fef5d38819c5402d0dcc389f86 Mon Sep 17 00:00:00 2001 From: Andreas Bauer Date: Sun, 24 Nov 2024 15:31:40 +0100 Subject: [PATCH 20/33] Use fix version --- ENGAGEHF.xcodeproj/project.pbxproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ENGAGEHF.xcodeproj/project.pbxproj b/ENGAGEHF.xcodeproj/project.pbxproj index c3c61ec4..3b6595de 100644 --- a/ENGAGEHF.xcodeproj/project.pbxproj +++ b/ENGAGEHF.xcodeproj/project.pbxproj @@ -2063,8 +2063,8 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/StanfordSpezi/SpeziAccount.git"; requirement = { - kind = upToNextMajorVersion; - minimumVersion = 2.1.0; + branch = "fix/wait-for-complete-details"; + kind = branch; }; }; 2FE5DC6529EDD894004B9AB4 /* XCRemoteSwiftPackageReference "SpeziContact" */ = { From bba24ba4465e192ed07143fb6e6ea5a2f15ad9ed Mon Sep 17 00:00:00 2001 From: nriedman <108841122+nriedman@users.noreply.github.com> Date: Tue, 3 Dec 2024 11:10:20 -0800 Subject: [PATCH 21/33] Package.resolved and localizable strings --- .../xcshareddata/swiftpm/Package.resolved | 4 ++-- ENGAGEHF/Resources/Localizable.xcstrings | 6 ------ 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/ENGAGEHF.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ENGAGEHF.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 8e51f352..09bd725f 100644 --- a/ENGAGEHF.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/ENGAGEHF.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -195,8 +195,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/StanfordSpezi/SpeziAccount.git", "state" : { - "revision" : "0e4dcc7d3284b439b17fae621c5c6e73d9213696", - "version" : "2.1.0" + "branch" : "fix/wait-for-complete-details", + "revision" : "a7e3df1b51d31771eccb8c2c624e46ef0b33153e" } }, { diff --git a/ENGAGEHF/Resources/Localizable.xcstrings b/ENGAGEHF/Resources/Localizable.xcstrings index 65eb1407..b63805ee 100644 --- a/ENGAGEHF/Resources/Localizable.xcstrings +++ b/ENGAGEHF/Resources/Localizable.xcstrings @@ -188,9 +188,6 @@ }, "Enable Notifications in Settings" : { - }, - "ENGAGE-HF" : { - }, "ENGAGE-HF Application Loading Screen" : { @@ -478,9 +475,6 @@ }, "Please enter your invitation code to join the ENGAGE-HF study." : { - }, - "Preparing your Account" : { - }, "Process timed out." : { From e58f70fd62372f2c13869f64f7468fcddb45ac82 Mon Sep 17 00:00:00 2001 From: nriedman <108841122+nriedman@users.noreply.github.com> Date: Tue, 3 Dec 2024 13:04:17 -0800 Subject: [PATCH 22/33] Change expectedSheetContent logic --- ENGAGEHF/ContentView.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ENGAGEHF/ContentView.swift b/ENGAGEHF/ContentView.swift index 904e2859..e7c5663b 100644 --- a/ENGAGEHF/ContentView.swift +++ b/ENGAGEHF/ContentView.swift @@ -36,8 +36,8 @@ struct ContentView: View { return .auth } guard FeatureFlags.disableFirebase - || account?.details?.isIncomplete ?? true - || account?.details?.invitationCode != nil else { + || (account?.details?.isIncomplete ?? true + && account?.details?.invitationCode != nil) else { return .auth } return nil From 1c976dec11a102a71c0cc61bfcc54473f2ea8c4a Mon Sep 17 00:00:00 2001 From: nriedman <108841122+nriedman@users.noreply.github.com> Date: Tue, 3 Dec 2024 13:04:39 -0800 Subject: [PATCH 23/33] Control timezone in recent vitals section tests --- .../Vitals/RecentVitalsSection.swift | 10 ++++++++- .../Account/OnboardingUITests.swift | 2 +- .../Dashboard/RecentVitalsUITests.swift | 21 ++++++++++++++++--- 3 files changed, 28 insertions(+), 5 deletions(-) diff --git a/ENGAGEHF/Dashboard/Vitals/RecentVitalsSection.swift b/ENGAGEHF/Dashboard/Vitals/RecentVitalsSection.swift index f85abb20..d8b91941 100644 --- a/ENGAGEHF/Dashboard/Vitals/RecentVitalsSection.swift +++ b/ENGAGEHF/Dashboard/Vitals/RecentVitalsSection.swift @@ -31,9 +31,17 @@ struct RecentVitalsSection: View { return nil } +#if TEST + var formatStyle: Date.FormatStyle = .dateTime + formatStyle.timeZone = TimeZone(identifier: "UTC")! + let formattedDate = measurement.startDate.formatted(formatStyle) +#else + let formattedDate = measurement.startDate.formatted(date: .numeric, time: .shortened) +#endif + return ( String(format: "%.1f", measurement.quantity.doubleValue(for: massUnits)), - measurement.startDate.formatted(date: .numeric, time: .shortened) + formattedDate ) } diff --git a/ENGAGEHFUITests/Account/OnboardingUITests.swift b/ENGAGEHFUITests/Account/OnboardingUITests.swift index f704d704..2a8cd369 100644 --- a/ENGAGEHFUITests/Account/OnboardingUITests.swift +++ b/ENGAGEHFUITests/Account/OnboardingUITests.swift @@ -27,7 +27,7 @@ final class OnboardingUITests: XCTestCase { } let app = XCUIApplication() - app.launchArguments = ["--showOnboarding", "--useFirebaseEmulator"] + app.launchArguments = ["--useFirebaseEmulator"] app.launch() } diff --git a/ENGAGEHFUITests/Dashboard/RecentVitalsUITests.swift b/ENGAGEHFUITests/Dashboard/RecentVitalsUITests.swift index 1d095e42..1b3292e4 100644 --- a/ENGAGEHFUITests/Dashboard/RecentVitalsUITests.swift +++ b/ENGAGEHFUITests/Dashboard/RecentVitalsUITests.swift @@ -6,8 +6,10 @@ // SPDX-License-Identifier: MIT // +import Foundation import XCTest + final class RecentVitalsUITests: XCTestCase { override func setUpWithError() throws { try super.setUpWithError() @@ -53,12 +55,18 @@ final class RecentVitalsUITests: XCTestCase { XCTAssertFalse(app.alerts.element.exists) + let expectedDateComponents = DateComponents(year: 2024, month: 6, day: 5, hour: 12, minute: 33, second: 11) + let expectedDate = Calendar.current.date(from: expectedDateComponents) ?? .now + + var formatStyle: Date.FormatStyle = .dateTime + formatStyle.timeZone = TimeZone(identifier: "UTC")! + let expectedFormattedDate = expectedDate.formatted(formatStyle) // 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: \(expectedFormattedDate)"].exists) app.staticTexts["Weight Quantity: \(expectedWeight)"].tap() XCTAssert(app.staticTexts["Body Weight"].waitForExistence(timeout: 2.0)) @@ -100,10 +108,17 @@ final class RecentVitalsUITests: XCTestCase { // Measurement has been successfully saved, and should be represented in the dashboard XCTAssert(app.staticTexts["Recent Vitals"].waitForExistence(timeout: 2.0)) + let expectedDateComponents = DateComponents(year: 2024, month: 6, day: 5, hour: 12, minute: 33, second: 11) + let expectedDate = Calendar.current.date(from: expectedDateComponents) ?? .now + + var formatStyle: Date.FormatStyle = .dateTime + formatStyle.timeZone = TimeZone(identifier: "UTC")! + let expectedFormattedDate = expectedDate.formatted(formatStyle) + 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: \(expectedFormattedDate)"].exists) app.staticTexts[heartRateQuantityText].tap() XCTAssert(app.staticTexts["Heart Rate"].waitForExistence(timeout: 2.0)) @@ -114,7 +129,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: \(expectedFormattedDate)"].exists) app.staticTexts[bloodPressureQuantityText].tap() XCTAssert(app.staticTexts["Blood Pressure"].waitForExistence(timeout: 2.0)) From 77c3cda280bb51576f8a4e0c84aa1de3bb15e593 Mon Sep 17 00:00:00 2001 From: nriedman <108841122+nriedman@users.noreply.github.com> Date: Tue, 3 Dec 2024 13:13:51 -0800 Subject: [PATCH 24/33] Correct logic for expectedSheetContent. --- ENGAGEHF/ContentView.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ENGAGEHF/ContentView.swift b/ENGAGEHF/ContentView.swift index e7c5663b..05a59ba1 100644 --- a/ENGAGEHF/ContentView.swift +++ b/ENGAGEHF/ContentView.swift @@ -36,8 +36,7 @@ struct ContentView: View { return .auth } guard FeatureFlags.disableFirebase - || (account?.details?.isIncomplete ?? true - && account?.details?.invitationCode != nil) else { + || (!(account?.details?.isIncomplete ?? true) && (account?.details?.invitationCode != nil)) else { return .auth } return nil From 64d7d0b963c1e0926035ce98bacb49f2219fe310 Mon Sep 17 00:00:00 2001 From: nriedman <108841122+nriedman@users.noreply.github.com> Date: Tue, 3 Dec 2024 17:51:36 -0800 Subject: [PATCH 25/33] Update logic for expectedSheetView --- ENGAGEHF/ContentView.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ENGAGEHF/ContentView.swift b/ENGAGEHF/ContentView.swift index 05a59ba1..66e70a5a 100644 --- a/ENGAGEHF/ContentView.swift +++ b/ENGAGEHF/ContentView.swift @@ -36,7 +36,8 @@ struct ContentView: View { return .auth } guard FeatureFlags.disableFirebase - || (!(account?.details?.isIncomplete ?? true) && (account?.details?.invitationCode != nil)) else { + || !(account?.details?.isIncomplete ?? true) + || account?.details?.invitationCode != nil else { return .auth } return nil From 7c69924253e2ea054d4ea5d33511a3bea2a1c96b Mon Sep 17 00:00:00 2001 From: Paul Schmiedmayer Date: Tue, 3 Dec 2024 23:47:00 -0800 Subject: [PATCH 26/33] Update UI Tests --- .../Dashboard/RecentVitalsUITests.swift | 30 ++++++++----------- .../Education/EducationViewUITests.swift | 3 -- .../HeartHealth/HeartHealthUITests.swift | 5 ---- .../Medications/MedicationsUITests.swift | 3 -- 4 files changed, 13 insertions(+), 28 deletions(-) diff --git a/ENGAGEHFUITests/Dashboard/RecentVitalsUITests.swift b/ENGAGEHFUITests/Dashboard/RecentVitalsUITests.swift index 1b3292e4..47d6186a 100644 --- a/ENGAGEHFUITests/Dashboard/RecentVitalsUITests.swift +++ b/ENGAGEHFUITests/Dashboard/RecentVitalsUITests.swift @@ -11,6 +11,16 @@ 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() @@ -55,18 +65,11 @@ final class RecentVitalsUITests: XCTestCase { XCTAssertFalse(app.alerts.element.exists) - let expectedDateComponents = DateComponents(year: 2024, month: 6, day: 5, hour: 12, minute: 33, second: 11) - let expectedDate = Calendar.current.date(from: expectedDateComponents) ?? .now - - var formatStyle: Date.FormatStyle = .dateTime - formatStyle.timeZone = TimeZone(identifier: "UTC")! - let expectedFormattedDate = expectedDate.formatted(formatStyle) - // 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: \(expectedFormattedDate)"].exists) + XCTAssert(app.staticTexts["Weight Date: \(expectedFormattedMeasurementDate)"].exists) app.staticTexts["Weight Quantity: \(expectedWeight)"].tap() XCTAssert(app.staticTexts["Body Weight"].waitForExistence(timeout: 2.0)) @@ -108,17 +111,10 @@ final class RecentVitalsUITests: XCTestCase { // Measurement has been successfully saved, and should be represented in the dashboard XCTAssert(app.staticTexts["Recent Vitals"].waitForExistence(timeout: 2.0)) - let expectedDateComponents = DateComponents(year: 2024, month: 6, day: 5, hour: 12, minute: 33, second: 11) - let expectedDate = Calendar.current.date(from: expectedDateComponents) ?? .now - - var formatStyle: Date.FormatStyle = .dateTime - formatStyle.timeZone = TimeZone(identifier: "UTC")! - let expectedFormattedDate = expectedDate.formatted(formatStyle) - 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: \(expectedFormattedDate)"].exists) + XCTAssert(app.staticTexts["Heart Rate Date: \(expectedFormattedMeasurementDate)"].exists) app.staticTexts[heartRateQuantityText].tap() XCTAssert(app.staticTexts["Heart Rate"].waitForExistence(timeout: 2.0)) @@ -129,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: \(expectedFormattedDate)"].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 caf8f1dd..acbc315d 100644 --- a/ENGAGEHFUITests/Education/EducationViewUITests.swift +++ b/ENGAGEHFUITests/Education/EducationViewUITests.swift @@ -18,7 +18,6 @@ final class EducationViewUITests: XCTestCase { let app = XCUIApplication() app.launchArguments = ["--assumeOnboardingComplete", "--setupTestEnvironment", "--useFirebaseEmulator", "--setupTestVideos"] app.launch() - setupSnapshot(app) } @@ -32,7 +31,6 @@ final class EducationViewUITests: XCTestCase { let thumbnailOverlay = app.staticTexts["Thumbnail Overlay Title: Long Description"] XCTAssert(thumbnailOverlay.waitForExistence(timeout: 0.5)) - snapshot("5Education") thumbnailOverlay.tap() sleep(2) @@ -70,7 +68,6 @@ final class EducationViewUITests: XCTestCase { let descriptionText = scrollableText.staticTexts["Scrollable Text"] XCTAssert(descriptionText.exists) XCTAssertEqual(descriptionText.label, expectedDescription) - snapshot("EducationVideo") } diff --git a/ENGAGEHFUITests/HeartHealth/HeartHealthUITests.swift b/ENGAGEHFUITests/HeartHealth/HeartHealthUITests.swift index 6a0829d1..3c1cd622 100644 --- a/ENGAGEHFUITests/HeartHealth/HeartHealthUITests.swift +++ b/ENGAGEHFUITests/HeartHealth/HeartHealthUITests.swift @@ -23,7 +23,6 @@ final class HeartHealthUITests: XCTestCase { "--useFirebaseEmulator" ] app.launch() - setupSnapshot(app) } func testSymptomScores() throws { @@ -256,10 +255,6 @@ extension XCUIApplication { XCTAssert(buttons["Discard"].exists) XCTAssert(buttons["Save"].exists) - - if displayName == "Blood Pressure" { - ENGAGEHFUITests.snapshot("3BloodPressureMeasurement") - } buttons["Save"].tap() sleep(1) diff --git a/ENGAGEHFUITests/Medications/MedicationsUITests.swift b/ENGAGEHFUITests/Medications/MedicationsUITests.swift index 1e9d6c31..b65fed27 100644 --- a/ENGAGEHFUITests/Medications/MedicationsUITests.swift +++ b/ENGAGEHFUITests/Medications/MedicationsUITests.swift @@ -19,7 +19,6 @@ final class MedicationsUITests: XCTestCase { let app = XCUIApplication() app.launchArguments = ["--skipOnboarding", "--setupTestEnvironment", "--useFirebaseEmulator", "--setupTestMedications"] app.launch() - setupSnapshot(app) } @@ -148,8 +147,6 @@ final class MedicationsUITests: XCTestCase { XCTAssert(app.staticTexts["5"].waitForExistence(timeout: 0.5), "Second component of current dose not found.") XCTAssert(app.staticTexts["mg"].firstMatch.waitForExistence(timeout: 0.5), "Units not found.") XCTAssert(app.staticTexts["daily"].firstMatch.waitForExistence(timeout: 0.5), "\"Daily\" quantifier not found.") - - snapshot("4Medications") } func testFrequencyStyling() throws { From 352e4c2c863cbbb69eb7031cbd6f38ac5a9b1167 Mon Sep 17 00:00:00 2001 From: Paul Schmiedmayer Date: Tue, 3 Dec 2024 23:50:06 -0800 Subject: [PATCH 27/33] Fix Swiftlint --- ENGAGEHF/Dashboard/Vitals/RecentVitalsSection.swift | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/ENGAGEHF/Dashboard/Vitals/RecentVitalsSection.swift b/ENGAGEHF/Dashboard/Vitals/RecentVitalsSection.swift index d8b91941..f85abb20 100644 --- a/ENGAGEHF/Dashboard/Vitals/RecentVitalsSection.swift +++ b/ENGAGEHF/Dashboard/Vitals/RecentVitalsSection.swift @@ -31,17 +31,9 @@ struct RecentVitalsSection: View { return nil } -#if TEST - var formatStyle: Date.FormatStyle = .dateTime - formatStyle.timeZone = TimeZone(identifier: "UTC")! - let formattedDate = measurement.startDate.formatted(formatStyle) -#else - let formattedDate = measurement.startDate.formatted(date: .numeric, time: .shortened) -#endif - return ( String(format: "%.1f", measurement.quantity.doubleValue(for: massUnits)), - formattedDate + measurement.startDate.formatted(date: .numeric, time: .shortened) ) } From 2a5899bf791c9169395beedb2bc57afef8191b2f Mon Sep 17 00:00:00 2001 From: Paul Schmiedmayer Date: Tue, 3 Dec 2024 23:51:26 -0800 Subject: [PATCH 28/33] Reduce Diff --- ENGAGEHFUITests/Account/OnboardingUITests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ENGAGEHFUITests/Account/OnboardingUITests.swift b/ENGAGEHFUITests/Account/OnboardingUITests.swift index 2a8cd369..f704d704 100644 --- a/ENGAGEHFUITests/Account/OnboardingUITests.swift +++ b/ENGAGEHFUITests/Account/OnboardingUITests.swift @@ -27,7 +27,7 @@ final class OnboardingUITests: XCTestCase { } let app = XCUIApplication() - app.launchArguments = ["--useFirebaseEmulator"] + app.launchArguments = ["--showOnboarding", "--useFirebaseEmulator"] app.launch() } From 60908e56595195fd6b4e19b5760088c373c019f5 Mon Sep 17 00:00:00 2001 From: Paul Schmiedmayer Date: Tue, 3 Dec 2024 23:53:43 -0800 Subject: [PATCH 29/33] Update Testing Setup --- ENGAGEHF/Onboarding/InvitationCodeView.swift | 1 - fastlane/Fastfile | 3 --- 2 files changed, 4 deletions(-) diff --git a/ENGAGEHF/Onboarding/InvitationCodeView.swift b/ENGAGEHF/Onboarding/InvitationCodeView.swift index 72d1b20c..937b72e8 100644 --- a/ENGAGEHF/Onboarding/InvitationCodeView.swift +++ b/ENGAGEHF/Onboarding/InvitationCodeView.swift @@ -16,7 +16,6 @@ import SwiftUI struct InvitationCodeView: View { @Environment(OnboardingNavigationPath.self) private var onboardingNavigationPath @Environment(InvitationCodeModule.self) private var invitationCodeModule - @Environment(AccountNotifications.self) private var accountNotifications @Environment(Account.self) private var account @State private var invitationCode = "" diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 9f662460..240b9b80 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -21,9 +21,6 @@ platform :ios do derived_data_path: ".derivedData", code_coverage: true, devices: ["iPhone 16 Plus"], - force_quit_simulator: true, - reset_simulator: true, - prelaunch_simulator: false, concurrent_workers: 1, max_concurrent_simulators: 1, result_bundle: true, From 0b539a9e10a1258e70102684b64bdce7744532df Mon Sep 17 00:00:00 2001 From: Paul Schmiedmayer Date: Tue, 3 Dec 2024 23:54:12 -0800 Subject: [PATCH 30/33] Fix Diff --- fastlane/Fastfile | 1 + 1 file changed, 1 insertion(+) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 240b9b80..ec0f520a 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -21,6 +21,7 @@ platform :ios do derived_data_path: ".derivedData", code_coverage: true, devices: ["iPhone 16 Plus"], + disable_slide_to_type: false, concurrent_workers: 1, max_concurrent_simulators: 1, result_bundle: true, From 95b95e203591858563967a4c6a24bfc3240877d7 Mon Sep 17 00:00:00 2001 From: nriedman <108841122+nriedman@users.noreply.github.com> Date: Fri, 6 Dec 2024 14:27:47 -0800 Subject: [PATCH 31/33] Refreshable content protocol, disable chart interactions --- ENGAGEHF.xcodeproj/project.pbxproj | 8 +++-- .../xcshareddata/xcschemes/ENGAGEHF.xcscheme | 4 +-- ENGAGEHF/Account/InvitationCodeModule.swift | 19 +++++++++++ .../Symptoms/SymptomsGraphSection.swift | 1 + .../Vitals/VitalsGraphSection.swift | 1 + ENGAGEHF/Managers/ManagerProtocol.swift | 21 ++++++++++++ .../MedicationsManager.swift | 10 +++++- .../MessageManager/MessageManager.swift | 13 +++++--- .../NavigationManager/NavigationManager.swift | 32 ++++++++++++------- .../NotificationManager.swift | 27 ++++++++++++++-- .../UserMetaDataManager.swift | 11 ++++++- .../Managers/VideoManager/VideoManager.swift | 13 +++++--- .../VitalsManager/VitalsManager.swift | 11 ++++++- 13 files changed, 141 insertions(+), 30 deletions(-) create mode 100644 ENGAGEHF/Managers/ManagerProtocol.swift diff --git a/ENGAGEHF.xcodeproj/project.pbxproj b/ENGAGEHF.xcodeproj/project.pbxproj index 3b6595de..697beb0c 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 */; }; @@ -248,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 = ""; }; @@ -571,6 +573,7 @@ 4D081A212C6C2BCB00E13BC8 /* Managers */ = { isa = PBXGroup; children = ( + 21B93B772D039E1B0034EC86 /* ManagerProtocol.swift */, 4D8CE0F42C752D1C00560327 /* VideoManager */, 4D081A242C6C2C4500E13BC8 /* UserMetaDataManager */, 4D081A232C6C2BF200E13BC8 /* MedicationsManager */, @@ -712,8 +715,8 @@ 4D62BF372C6A97490088C414 /* NotificationManager */ = { isa = PBXGroup; children = ( - 4D8CE0FA2C753A4600560327 /* NotificationManager.swift */, 4D8CE0FB2C753A4600560327 /* NotificationRegistrationSchema.swift */, + 4D8CE0FA2C753A4600560327 /* NotificationManager.swift */, ); path = NotificationManager; sourceTree = ""; @@ -1041,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 = ""; @@ -1448,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 */, diff --git a/ENGAGEHF.xcodeproj/xcshareddata/xcschemes/ENGAGEHF.xcscheme b/ENGAGEHF.xcodeproj/xcshareddata/xcschemes/ENGAGEHF.xcscheme index f51da6dc..9ee9963c 100644 --- a/ENGAGEHF.xcodeproj/xcshareddata/xcschemes/ENGAGEHF.xcscheme +++ b/ENGAGEHF.xcodeproj/xcshareddata/xcschemes/ENGAGEHF.xcscheme @@ -119,7 +119,7 @@ + isEnabled = "YES"> + isEnabled = "YES"> diff --git a/ENGAGEHF/Account/InvitationCodeModule.swift b/ENGAGEHF/Account/InvitationCodeModule.swift index 292f11ec..603cc4e7 100644 --- a/ENGAGEHF/Account/InvitationCodeModule.swift +++ b/ENGAGEHF/Account/InvitationCodeModule.swift @@ -20,6 +20,15 @@ class InvitationCodeModule: Module, EnvironmentAccessible { @Dependency(Account.self) private var account: Account? @Dependency(FirebaseAccountService.self) private var accountService: FirebaseAccountService? + + @Dependency(VideoManager.self) private var videoManager + @Dependency(UserMetaDataManager.self) private var userMetaDataManager + @Dependency(MedicationsManager.self) private var medicationsManager + @Dependency(NotificationManager.self) private var notificationManager + @Dependency(MessageManager.self) private var messageManager + @Dependency(NavigationManager.self) private var navigationManager + @Dependency(VitalsManager.self) private var vitalsManager + func configure() { if FeatureFlags.useFirebaseEmulator && !FeatureFlags.disableFirebase { @@ -42,6 +51,16 @@ class InvitationCodeModule: Module, EnvironmentAccessible { let enrollUser = Functions.functions().httpsCallable("enrollUser") _ = try await enrollUser.call(["invitationCode": invitationCode]) _ = try? await Auth.auth().currentUser?.getIDToken(forcingRefresh: true) + + // Now that we've forced refresh on the auth token, refresh the content of the managers. + await videoManager.refreshContent() + await userMetaDataManager.refreshContent() + await medicationsManager.refreshContent() + await notificationManager.refreshContent() + await messageManager.refreshContent() + await navigationManager.refreshContent() + await vitalsManager.refreshContent() + logger.debug("Successfully enrolled user!") } catch { logger.error("Failed to enroll user: \(error)") 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 839c612e..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,6 +36,10 @@ 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 { @@ -53,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 53712e2e..55536fae 100644 --- a/ENGAGEHF/Managers/NotificationManager/NotificationManager.swift +++ b/ENGAGEHF/Managers/NotificationManager/NotificationManager.swift @@ -22,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." @@ -47,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 @@ -92,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...") From bf83d08bfee73f564ec9ba0910e1f515fc1be339 Mon Sep 17 00:00:00 2001 From: Andreas Bauer Date: Sun, 8 Dec 2024 23:29:27 +0100 Subject: [PATCH 32/33] Upgrade to SpeziFirebase fix --- ENGAGEHF.xcodeproj/project.pbxproj | 4 +-- .../xcshareddata/swiftpm/Package.resolved | 36 +++++++++---------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/ENGAGEHF.xcodeproj/project.pbxproj b/ENGAGEHF.xcodeproj/project.pbxproj index 697beb0c..fee53d2a 100644 --- a/ENGAGEHF.xcodeproj/project.pbxproj +++ b/ENGAGEHF.xcodeproj/project.pbxproj @@ -2091,8 +2091,8 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/StanfordSpezi/SpeziFirebase.git"; requirement = { - kind = upToNextMajorVersion; - minimumVersion = 2.0.0; + branch = "fix/storage-never-returns"; + kind = branch; }; }; 2FE5DC8229EDD934004B9AB4 /* XCRemoteSwiftPackageReference "SpeziQuestionnaire" */ = { diff --git a/ENGAGEHF.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ENGAGEHF.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 09bd725f..9e25bae9 100644 --- a/ENGAGEHF.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/ENGAGEHF.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -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" @@ -60,8 +60,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/firebase/firebase-ios-sdk.git", "state" : { - "revision" : "dbdfdc44bee8b8e4eaa5ec27eb12b9338f3f2bc1", - "version" : "11.5.0" + "revision" : "2e02253fd1ce99145bcbf1bb367ccf61bd0ca46b", + "version" : "11.6.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" } }, { @@ -196,7 +196,7 @@ "location" : "https://github.com/StanfordSpezi/SpeziAccount.git", "state" : { "branch" : "fix/wait-for-complete-details", - "revision" : "a7e3df1b51d31771eccb8c2c624e46ef0b33153e" + "revision" : "e8698a3cda2b056419305f2dc97705f3d41ffe81" } }, { @@ -231,8 +231,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/StanfordSpezi/SpeziFirebase.git", "state" : { - "revision" : "7c6829624884f6f1d700e0316b2580b39d3b0c5f", - "version" : "2.0.0" + "branch" : "fix/storage-never-returns", + "revision" : "0c64273de2376b3a6d55b60af3b1d22847b5c0b6" } }, { @@ -319,7 +319,7 @@ { "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" @@ -348,8 +348,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-nio.git", "state" : { - "revision" : "914081701062b11e3bb9e21accc379822621995e", - "version" : "2.76.1" + "revision" : "dca6594f65308c761a9c409e09fbf35f48d50d34", + "version" : "2.77.0" } }, { @@ -357,8 +357,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/FelixHerrmann/swift-package-list", "state" : { - "revision" : "26732b1cf7e422cb330a1e24420394752a14b059", - "version" : "4.4.1" + "revision" : "5e954ec39ce2374ff28a38224fd4e6bba7c57cdc", + "version" : "4.4.2" } }, { @@ -375,8 +375,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/swiftlang/swift-syntax.git", "state" : { - "revision" : "515f79b522918f83483068d99c68daeb5116342d", - "version" : "600.0.0-prerelease-2024-08-14" + "revision" : "cb53fa1bd3219b0b23ded7dfdd3b2baff266fd25", + "version" : "600.0.0" } }, { @@ -393,8 +393,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/realm/SwiftLint.git", "state" : { - "revision" : "168fb98ed1f3e343d703ecceaf518b6cf565207b", - "version" : "0.57.0" + "revision" : "25f2776977e663305bee71309ea1e34d435065f1", + "version" : "0.57.1" } }, { @@ -436,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" From 262a5ccc0a53212939ef509ec59645c70ebb5bcd Mon Sep 17 00:00:00 2001 From: Andreas Bauer Date: Mon, 9 Dec 2024 19:19:26 +0100 Subject: [PATCH 33/33] Release versions --- ENGAGEHF.xcodeproj/project.pbxproj | 8 ++++---- .../xcshareddata/swiftpm/Package.resolved | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/ENGAGEHF.xcodeproj/project.pbxproj b/ENGAGEHF.xcodeproj/project.pbxproj index fee53d2a..c7cd868b 100644 --- a/ENGAGEHF.xcodeproj/project.pbxproj +++ b/ENGAGEHF.xcodeproj/project.pbxproj @@ -2067,8 +2067,8 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/StanfordSpezi/SpeziAccount.git"; requirement = { - branch = "fix/wait-for-complete-details"; - kind = branch; + kind = upToNextMajorVersion; + minimumVersion = 2.1.1; }; }; 2FE5DC6529EDD894004B9AB4 /* XCRemoteSwiftPackageReference "SpeziContact" */ = { @@ -2091,8 +2091,8 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/StanfordSpezi/SpeziFirebase.git"; requirement = { - branch = "fix/storage-never-returns"; - kind = branch; + kind = upToNextMajorVersion; + minimumVersion = 2.0.1; }; }; 2FE5DC8229EDD934004B9AB4 /* XCRemoteSwiftPackageReference "SpeziQuestionnaire" */ = { diff --git a/ENGAGEHF.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ENGAGEHF.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 9e25bae9..d71af38a 100644 --- a/ENGAGEHF.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/ENGAGEHF.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -195,8 +195,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/StanfordSpezi/SpeziAccount.git", "state" : { - "branch" : "fix/wait-for-complete-details", - "revision" : "e8698a3cda2b056419305f2dc97705f3d41ffe81" + "revision" : "37df11e8f65b9aa0a3aaf0d4bc3e65e87b425637", + "version" : "2.1.1" } }, { @@ -231,8 +231,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/StanfordSpezi/SpeziFirebase.git", "state" : { - "branch" : "fix/storage-never-returns", - "revision" : "0c64273de2376b3a6d55b60af3b1d22847b5c0b6" + "revision" : "5dd57f9de42c02d6a94f3af4d8cf3d9b81ec6661", + "version" : "2.0.1" } }, {