From 84cfd2d4ee8c907af058f23caf6c7224c09f0852 Mon Sep 17 00:00:00 2001 From: Vishnu Ravi Date: Mon, 10 Jun 2024 10:14:09 -0400 Subject: [PATCH 1/3] Updates survey --- StrokeCog/Resources/Localizable.xcstrings | 21 +++ StrokeCog/SharedContext/StorageKeys.swift | 2 + StrokeCog/Survey/DailySurveyResponse.swift | 1 + StrokeCog/Survey/DailySurveyTask.swift | 21 ++- StrokeCog/Survey/DailySurveyTaskView.swift | 151 ++++++++++++++++++--- 5 files changed, 170 insertions(+), 26 deletions(-) diff --git a/StrokeCog/Resources/Localizable.xcstrings b/StrokeCog/Resources/Localizable.xcstrings index 4c74a90..d54963a 100644 --- a/StrokeCog/Resources/Localizable.xcstrings +++ b/StrokeCog/Resources/Localizable.xcstrings @@ -132,6 +132,9 @@ } } } + }, + "CONTINUE" : { + }, "Error" : { @@ -348,6 +351,9 @@ } } } + }, + "OK" : { + }, "OPTIONS_PANEL_SURVEY_BUTTON" : { "localizations" : { @@ -368,6 +374,9 @@ } } } + }, + "Please note that you are about to take yesterday's survey." : { + }, "Refresh map" : { @@ -384,6 +393,9 @@ } } } + }, + "Saving survey..." : { + }, "SCHEDULE_LIST_TITLE" : { "localizations" : { @@ -456,6 +468,9 @@ } } } + }, + "Survey already taken." : { + }, "Survey not available." : { @@ -620,6 +635,12 @@ } } } + }, + "Yesterday's survey." : { + + }, + "You've already taken the latest available survey." : { + } }, "version" : "1.0" diff --git a/StrokeCog/SharedContext/StorageKeys.swift b/StrokeCog/SharedContext/StorageKeys.swift index 4d7f953..45c1900 100644 --- a/StrokeCog/SharedContext/StorageKeys.swift +++ b/StrokeCog/SharedContext/StorageKeys.swift @@ -22,4 +22,6 @@ enum StorageKeys { static let homeTabSelection = "home.tabselection" static let trackingPreference = "tracking.preference" + + static let lastSurveyDate = "lastSurveyDate" } diff --git a/StrokeCog/Survey/DailySurveyResponse.swift b/StrokeCog/Survey/DailySurveyResponse.swift index bed29e8..46f1af0 100644 --- a/StrokeCog/Survey/DailySurveyResponse.swift +++ b/StrokeCog/Survey/DailySurveyResponse.swift @@ -10,6 +10,7 @@ import Foundation struct DailySurveyResponse: Codable { var surveyName: String? + var surveyDate: String? var studyID: String? var updatedBy: String? var timestamp: Date? diff --git a/StrokeCog/Survey/DailySurveyTask.swift b/StrokeCog/Survey/DailySurveyTask.swift index 7801e70..f111d33 100644 --- a/StrokeCog/Survey/DailySurveyTask.swift +++ b/StrokeCog/Survey/DailySurveyTask.swift @@ -8,6 +8,7 @@ import Foundation import ResearchKit +// swiftlint:disable function_body_length class DailySurveyTask: ORKOrderedTask { convenience init(identifier: String) { // Initialize the array to hold the steps of the survey @@ -24,18 +25,23 @@ class DailySurveyTask: ORKOrderedTask { let question1Step = ORKQuestionStep( identifier: "SocialInteractionQuestion", title: "Social Interaction", - question: "How many close friends or family did you see today face to face?", + question: "How many people did you engage with face to face today?", answer: answerFormat1 ) steps.append(question1Step) - // Question 2: Times left house - let answerFormat2 = ORKAnswerFormat.integerAnswerFormat(withUnit: nil) - answerFormat2.minimum = 0 + // Question 2: Time outside house + let answerFormat2 = ORKAnswerFormat.choiceAnswerFormat(with: .singleChoice, textChoices: [ + ORKTextChoice(text: "None", value: 0 as NSNumber), + ORKTextChoice(text: "Less than 1 hour", value: 1 as NSNumber), + ORKTextChoice(text: "1-4 hours", value: 2 as NSNumber), + ORKTextChoice(text: "4 or more hours", value: 3 as NSNumber) + ]) + let question2Step = ORKQuestionStep( identifier: "LeavingTheHouseQuestion", title: "Leaving the House", - question: "How many times today did you leave your house and engage meaningfully with others?", + question: "How many hours did you spend out of your house and meaningfully engaged with others?", answer: answerFormat2 ) steps.append(question2Step) @@ -68,6 +74,11 @@ class DailySurveyTask: ORKOrderedTask { ) steps.append(question4Step) + let completionStep = ORKCompletionStep(identifier: "DailySurveyTaskCompletionStep") + completionStep.title = "Thank you!" + completionStep.text = "Tap done below to save your survey. Remember to take your survey daily!" + steps.append(completionStep) + // Initialize the ORKOrderedTask with the steps array self.init(identifier: identifier, steps: steps) } diff --git a/StrokeCog/Survey/DailySurveyTaskView.swift b/StrokeCog/Survey/DailySurveyTaskView.swift index f098356..4193c6c 100644 --- a/StrokeCog/Survey/DailySurveyTaskView.swift +++ b/StrokeCog/Survey/DailySurveyTaskView.swift @@ -15,20 +15,33 @@ struct DailySurveyTaskView: View { @Binding var showingSurvey: Bool @State private var didError = false @State private var errorMessage = "" + @State private var acknowledgedPreviousDaySurvey = false + @State private var savingSurvey = false var body: some View { - if shouldShowSurvey { + if surveyAlreadyTaken { + surveyTakenView + } else if isPreviousDaySurvey && !acknowledgedPreviousDaySurvey { + previousDaySurveyView + } else if shouldShowSurvey { Group { ORKOrderedTaskView(tasks: DailySurveyTask(identifier: "DailySurveyTask")) { result in guard case let .completed(taskResult) = result else { - showingSurvey.toggle() + showingSurvey = false return } Task { + savingSurvey = true await saveResponse(taskResult: taskResult) - showingSurvey.toggle() + savingSurvey = false + showingSurvey = false + } + } + .overlay { + if savingSurvey { + savingSurveyView } } } @@ -38,6 +51,56 @@ struct DailySurveyTaskView: View { } } + private var savingSurveyView: some View { + VStack { + Text("Saving survey...") + ProgressView() + } + .padding() + .background(Color(UIColor.systemBackground)) + .clipShape(.rect(cornerRadius: 10)) + } + + private var surveyTakenView: some View { + VStack { + Image(systemName: "exclamationmark.triangle.fill") + .resizable() + .frame(width: 50, height: 50) + .accessibilityLabel("Survey already taken.") + + Text("You've already taken the latest available survey.") + .font(.largeTitle) + .padding() + .multilineTextAlignment(.center) + + Button("OK") { + self.showingSurvey = false + } + .buttonStyle(.borderedProminent) + .padding(.top, 20) + } + } + + private var previousDaySurveyView: some View { + VStack { + Image(systemName: "exclamationmark.triangle.fill") + .resizable() + .frame(width: 50, height: 50) + .accessibilityLabel("Yesterday's survey.") + + Text("Please note that you are about to take yesterday's survey.") + .font(.largeTitle) + .padding() + .multilineTextAlignment(.center) + + Button("CONTINUE") { + self.acknowledgedPreviousDaySurvey = true + } + .buttonStyle(.borderedProminent) + .padding(.top, 20) + } + } + private var surveyUnavailableView: some View { VStack { Image(systemName: "clock.fill") @@ -58,12 +121,44 @@ struct DailySurveyTaskView: View { } } - private var shouldShowSurvey: Bool { - // TODO: Add check if survey has already been answered today + private var dateFormatter: DateFormatter { + let formatter = DateFormatter() + formatter.dateFormat = "MM/dd/yyyy" + return formatter + } + + private var currentHour: Int { + Calendar.current.component(.hour, from: Date()) + } + + private var isPreviousDaySurvey: Bool { + /// If the user is taking the survey before 7am, they should be informed that they are taking the + /// previous day's survey + currentHour < 7 + } + + private var surveyAlreadyTaken: Bool { + let lastSurveyDateString = UserDefaults.standard.string(forKey: StorageKeys.lastSurveyDate) + + // Determine the survey date based on the current time + let surveyDate: Date + if currentHour < 7 { + surveyDate = Calendar.current.date(byAdding: .day, value: -1, to: Date())?.startOfDay ?? Date().startOfDay + } else { + surveyDate = Date().startOfDay + } + + // Format the survey date to a string + let surveyDateString = dateFormatter.string(from: surveyDate) + // Compare the last survey date with the calculated survey date + return lastSurveyDateString == surveyDateString + } + + + private var shouldShowSurvey: Bool { /// The survey should only be shown if it between 7pm and 7am - let currentHour = Calendar.current.component(.hour, from: Date()) - return currentHour < 7 || currentHour >= 19 + currentHour < 7 || currentHour >= 19 } private func saveResponse(taskResult: ORKTaskResult) async { @@ -71,22 +166,33 @@ struct DailySurveyTaskView: View { response.surveyName = "dailySurveyTask" - if let socialInteractionQuestion = taskResult.stepResult(forStepIdentifier: "SocialInteractionQuestion")?.results { - let answer = socialInteractionQuestion[0] as? ORKScaleQuestionResult - if let result = answer?.scaleAnswer { - response.socialInteractionQuestion = Int(truncating: result) - } else { - response.socialInteractionQuestion = -1 - } + /// If the user is taking the survey before 7am, the `surveyDate` should reflect the previous day, + /// otherwise it should reflect the current day. + let surveyDate: Date + if currentHour < 7 { + surveyDate = Calendar.current.date(byAdding: .day, value: -1, to: Date())?.startOfDay ?? Date().startOfDay + } else { + surveyDate = Date().startOfDay } + let surveyDateString = dateFormatter.string(from: surveyDate) + response.surveyDate = surveyDateString - if let leavingTheHouseQuestion = taskResult.stepResult(forStepIdentifier: "LeavingTheHouseQuestion")?.results { - let answer = leavingTheHouseQuestion[0] as? ORKNumericQuestionResult - if let result = answer?.numericAnswer { - response.leavingTheHouseQuestion = Int(truncating: result) - } else { - response.leavingTheHouseQuestion = -1 - } + // swiftlint:disable legacy_objc_type + /// Each answer is coded as a number, with -1 representing a question that was skipped. + if let socialInteractionQuestion = taskResult.stepResult(forStepIdentifier: "SocialInteractionQuestion"), + let result = socialInteractionQuestion.firstResult as? ORKChoiceQuestionResult, + let answer = result.choiceAnswers?.first as? NSNumber { + response.socialInteractionQuestion = answer.intValue + } else { + response.socialInteractionQuestion = -1 + } + + if let leavingTheHouseQuestion = taskResult.stepResult(forStepIdentifier: "LeavingTheHouseQuestion"), + let result = leavingTheHouseQuestion.firstResult as? ORKChoiceQuestionResult, + let answer = result.choiceAnswers?.first as? NSNumber { + response.leavingTheHouseQuestion = answer.intValue + } else { + response.leavingTheHouseQuestion = -1 } if let emotionalWellBeingQuestion = taskResult.stepResult(forStepIdentifier: "EmotionalWellBeingQuestion")?.results { @@ -106,6 +212,9 @@ struct DailySurveyTaskView: View { do { try await standard.add(response: response) + + // Update the last survey date in UserDefaults + UserDefaults.standard.set(surveyDateString, forKey: StorageKeys.lastSurveyDate) } catch { self.errorMessage = error.localizedDescription self.didError.toggle() From 88d4040455308cde5cf956adf0851cff4a1924cb Mon Sep 17 00:00:00 2001 From: Vishnu Ravi Date: Mon, 10 Jun 2024 16:11:51 -0400 Subject: [PATCH 2/3] Update strings --- StrokeCog.xcodeproj/project.pbxproj | 30 +++---- StrokeCog/Home.swift | 2 +- StrokeCog/Map/OptionsPanel.swift | 9 ++ StrokeCog/Map/RefreshIcon.swift | 2 +- StrokeCog/Map/StrokeCogMapView.swift | 2 +- StrokeCog/Onboarding/AccountOnboarding.swift | 7 ++ StrokeCog/Onboarding/Consent.swift | 1 + StrokeCog/Onboarding/StudyIDView.swift | 2 +- StrokeCog/Resources/Localizable.xcstrings | 86 ++++++++++++++------ StrokeCog/StrokeCogStandard.swift | 65 +++++++++++---- StrokeCog/Survey/DailySurveyTaskView.swift | 13 ++- 11 files changed, 152 insertions(+), 67 deletions(-) diff --git a/StrokeCog.xcodeproj/project.pbxproj b/StrokeCog.xcodeproj/project.pbxproj index 1d94426..7cf7669 100644 --- a/StrokeCog.xcodeproj/project.pbxproj +++ b/StrokeCog.xcodeproj/project.pbxproj @@ -776,11 +776,11 @@ INFOPLIST_FILE = "StrokeCog/Supporting Files/Info.plist"; INFOPLIST_KEY_CFBundleDisplayName = LifeSpace; INFOPLIST_KEY_NSCameraUsageDescription = "This message should never appear. Please adjust this when you start using camera information. We have to put this in here as ResearchKit has the possibility to use it and not putting it here returns an error on AppStore Connect."; - INFOPLIST_KEY_NSHealthShareUsageDescription = "The StrokeCog uses the step count to demonstrate Spezi's integration with HealthKit."; - INFOPLIST_KEY_NSHealthUpdateUsageDescription = "The StrokeCog uses the step count to demonstrate Spezi's integration with HealthKit."; - INFOPLIST_KEY_NSLocationAlwaysAndWhenInUseUsageDescription = "StrokeCog tracks your location for a study."; - INFOPLIST_KEY_NSLocationAlwaysUsageDescription = "StrokeCog tracks your location for a study."; - INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "StrokeCog tracks your location for a study."; + INFOPLIST_KEY_NSHealthShareUsageDescription = "The LifeSpace app collects health data for a research study."; + INFOPLIST_KEY_NSHealthUpdateUsageDescription = "The LifeSpace app collects health data for a research study."; + INFOPLIST_KEY_NSLocationAlwaysAndWhenInUseUsageDescription = "The LifeSpace app tracks your location for a study."; + INFOPLIST_KEY_NSLocationAlwaysUsageDescription = "The LifeSpace app tracks your location for a study."; + INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "The LifeSpace app tracks your location for a study."; INFOPLIST_KEY_NSMicrophoneUsageDescription = "This message should never appear. Please adjust this when you start using microphone information. We have to put this in here as ResearchKit has the possibility to use it and not putting it here returns an error on AppStore Connect."; INFOPLIST_KEY_NSMotionUsageDescription = "This message should never appear. Please adjust this when you start using motion information. We have to put this in here as ResearchKit has the possibility to use it and not putting it here returns an error on AppStore Connect."; INFOPLIST_KEY_NSSpeechRecognitionUsageDescription = "This message should never appear. Please adjust this when you start using speecg information. We have to put this in here as ResearchKit has the possibility to use it and not putting it here returns an error on AppStore Connect."; @@ -980,11 +980,11 @@ INFOPLIST_FILE = "StrokeCog/Supporting Files/Info.plist"; INFOPLIST_KEY_CFBundleDisplayName = LifeSpace; INFOPLIST_KEY_NSCameraUsageDescription = "This message should never appear. Please adjust this when you start using camera information. We have to put this in here as ResearchKit has the possibility to use it and not putting it here returns an error on AppStore Connect."; - INFOPLIST_KEY_NSHealthShareUsageDescription = "The StrokeCog uses the step count to demonstrate Spezi's integration with HealthKit."; - INFOPLIST_KEY_NSHealthUpdateUsageDescription = "The StrokeCog uses the step count to demonstrate Spezi's integration with HealthKit."; - INFOPLIST_KEY_NSLocationAlwaysAndWhenInUseUsageDescription = "StrokeCog tracks your location for a study."; - INFOPLIST_KEY_NSLocationAlwaysUsageDescription = "StrokeCog tracks your location for a study."; - INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "StrokeCog tracks your location for a study."; + INFOPLIST_KEY_NSHealthShareUsageDescription = "The LifeSpace app collects health data for a research study."; + INFOPLIST_KEY_NSHealthUpdateUsageDescription = "The LifeSpace app collects health data for a research study."; + INFOPLIST_KEY_NSLocationAlwaysAndWhenInUseUsageDescription = "The LifeSpace app tracks your location for a study."; + INFOPLIST_KEY_NSLocationAlwaysUsageDescription = "The LifeSpace app tracks your location for a study."; + INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "The LifeSpace app tracks your location for a study."; INFOPLIST_KEY_NSMicrophoneUsageDescription = "This message should never appear. Please adjust this when you start using microphone information. We have to put this in here as ResearchKit has the possibility to use it and not putting it here returns an error on AppStore Connect."; INFOPLIST_KEY_NSMotionUsageDescription = "This message should never appear. Please adjust this when you start using motion information. We have to put this in here as ResearchKit has the possibility to use it and not putting it here returns an error on AppStore Connect."; INFOPLIST_KEY_NSSpeechRecognitionUsageDescription = "This message should never appear. Please adjust this when you start using speecg information. We have to put this in here as ResearchKit has the possibility to use it and not putting it here returns an error on AppStore Connect."; @@ -1027,11 +1027,11 @@ INFOPLIST_FILE = "StrokeCog/Supporting Files/Info.plist"; INFOPLIST_KEY_CFBundleDisplayName = LifeSpace; INFOPLIST_KEY_NSCameraUsageDescription = "This message should never appear. Please adjust this when you start using camera information. We have to put this in here as ResearchKit has the possibility to use it and not putting it here returns an error on AppStore Connect."; - INFOPLIST_KEY_NSHealthShareUsageDescription = "The StrokeCog uses the step count to demonstrate Spezi's integration with HealthKit."; - INFOPLIST_KEY_NSHealthUpdateUsageDescription = "The StrokeCog uses the step count to demonstrate Spezi's integration with HealthKit."; - INFOPLIST_KEY_NSLocationAlwaysAndWhenInUseUsageDescription = "StrokeCog tracks your location for a study."; - INFOPLIST_KEY_NSLocationAlwaysUsageDescription = "StrokeCog tracks your location for a study."; - INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "StrokeCog tracks your location for a study."; + INFOPLIST_KEY_NSHealthShareUsageDescription = "The LifeSpace app collects health data for a research study."; + INFOPLIST_KEY_NSHealthUpdateUsageDescription = "The LifeSpace app collects health data for a research study."; + INFOPLIST_KEY_NSLocationAlwaysAndWhenInUseUsageDescription = "The LifeSpace app tracks your location for a study."; + INFOPLIST_KEY_NSLocationAlwaysUsageDescription = "The LifeSpace app tracks your location for a study."; + INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "The LifeSpace app tracks your location for a study."; INFOPLIST_KEY_NSMicrophoneUsageDescription = "This message should never appear. Please adjust this when you start using microphone information. We have to put this in here as ResearchKit has the possibility to use it and not putting it here returns an error on AppStore Connect."; INFOPLIST_KEY_NSMotionUsageDescription = "This message should never appear. Please adjust this when you start using motion information. We have to put this in here as ResearchKit has the possibility to use it and not putting it here returns an error on AppStore Connect."; INFOPLIST_KEY_NSSpeechRecognitionUsageDescription = "This message should never appear. Please adjust this when you start using speecg information. We have to put this in here as ResearchKit has the possibility to use it and not putting it here returns an error on AppStore Connect."; diff --git a/StrokeCog/Home.swift b/StrokeCog/Home.swift index 9cccd35..fae42f5 100644 --- a/StrokeCog/Home.swift +++ b/StrokeCog/Home.swift @@ -10,7 +10,7 @@ import SpeziAccount import SwiftUI -struct HomeView: View { +struct HomeView: View { static var accountEnabled: Bool { !FeatureFlags.disableFirebase && !FeatureFlags.skipOnboarding } diff --git a/StrokeCog/Map/OptionsPanel.swift b/StrokeCog/Map/OptionsPanel.swift index dbe2bb3..38a5b18 100644 --- a/StrokeCog/Map/OptionsPanel.swift +++ b/StrokeCog/Map/OptionsPanel.swift @@ -13,6 +13,8 @@ import SwiftUI struct OptionsPanel: View { @AppStorage(StorageKeys.trackingPreference) private var trackingOn = true @Environment(LocationModule.self) private var locationModule + @Environment(\.scenePhase) var scenePhase + @Environment(StrokeCogStandard.self) private var standard @State private var showingSurveyAlert = false @State private var showingSurvey = false @@ -28,6 +30,13 @@ struct OptionsPanel: View { .sheet(isPresented: $showingSurvey) { DailySurveyTaskView(showingSurvey: $showingSurvey) } + .onChange(of: scenePhase) { _, newPhase in + if newPhase == .active { + Task { + await standard.getLatestSurveyDate() + } + } + } } GroupBox { diff --git a/StrokeCog/Map/RefreshIcon.swift b/StrokeCog/Map/RefreshIcon.swift index 2cc4a8e..d23aba0 100644 --- a/StrokeCog/Map/RefreshIcon.swift +++ b/StrokeCog/Map/RefreshIcon.swift @@ -30,7 +30,7 @@ struct RefreshIcon: View { .onDisappear { rotationAngle = 0 } - .accessibilityLabel("Refreshing map") + .accessibilityLabel("REFRESHING_MAP") } } } diff --git a/StrokeCog/Map/StrokeCogMapView.swift b/StrokeCog/Map/StrokeCogMapView.swift index 579e2ee..ff79a42 100644 --- a/StrokeCog/Map/StrokeCogMapView.swift +++ b/StrokeCog/Map/StrokeCogMapView.swift @@ -53,7 +53,7 @@ struct StrokeCogMapView: View { refreshMap() }) { Image(systemName: "arrow.clockwise") - .accessibilityLabel("Refresh map") + .accessibilityLabel("REFRESHING_MAP") } } } diff --git a/StrokeCog/Onboarding/AccountOnboarding.swift b/StrokeCog/Onboarding/AccountOnboarding.swift index ddcdc68..8c7038e 100644 --- a/StrokeCog/Onboarding/AccountOnboarding.swift +++ b/StrokeCog/Onboarding/AccountOnboarding.swift @@ -12,6 +12,7 @@ import SwiftUI struct AccountOnboarding: View { + @Environment(StrokeCogStandard.self) private var standard @Environment(Account.self) private var account @Environment(OnboardingNavigationPath.self) private var onboardingNavigationPath @@ -23,6 +24,12 @@ struct AccountOnboarding: View { Task { // Placing the nextStep() call inside this task will ensure that the sheet dismiss animation is // played till the end before we navigate to the next step. + + // Now that the user is logged in, we will update the user document + if let studyID = UserDefaults.standard.string(forKey: StorageKeys.studyID) { + await standard.setStudyID(studyID) + } + onboardingNavigationPath.nextStep() } }, diff --git a/StrokeCog/Onboarding/Consent.swift b/StrokeCog/Onboarding/Consent.swift index 176b890..e6a9f67 100644 --- a/StrokeCog/Onboarding/Consent.swift +++ b/StrokeCog/Onboarding/Consent.swift @@ -52,6 +52,7 @@ struct Consent: View { self.savingConsentForms = true guard case let .completed(taskResult) = result else { + self.savingConsentForms = false self.isConsentSheetPresented = false return // user cancelled or task failed } diff --git a/StrokeCog/Onboarding/StudyIDView.swift b/StrokeCog/Onboarding/StudyIDView.swift index 43a3561..6b03149 100644 --- a/StrokeCog/Onboarding/StudyIDView.swift +++ b/StrokeCog/Onboarding/StudyIDView.swift @@ -60,7 +60,7 @@ struct StudyIDView: View { .validate(input: studyID, rules: [validationRule]) .receiveValidation(in: $validation) .alert( - "Error", + "ERROR", isPresented: $showInvalidIDAlert ) { Text("INVALID_STUDYID_MESSAGE") diff --git a/StrokeCog/Resources/Localizable.xcstrings b/StrokeCog/Resources/Localizable.xcstrings index d54963a..eaf95c1 100644 --- a/StrokeCog/Resources/Localizable.xcstrings +++ b/StrokeCog/Resources/Localizable.xcstrings @@ -134,10 +134,24 @@ } }, "CONTINUE" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "CONTINUE" + } + } + } }, - "Error" : { - + "ERROR" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "ERROR" + } + } + } }, "HEALTHKIT_PERMISSIONS_BUTTON" : { "localizations" : { @@ -353,7 +367,14 @@ } }, "OK" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "OK" + } + } + } }, "OPTIONS_PANEL_SURVEY_BUTTON" : { "localizations" : { @@ -375,14 +396,28 @@ } } }, - "Please note that you are about to take yesterday's survey." : { + "PREVIOUS_DAY_SURVEY_LABEL" : { }, - "Refresh map" : { - + "PREVIOUS_DAY_SURVEY_NOTICE" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Please note that you are answering yesterday's survey." + } + } + } }, - "Refreshing map" : { - + "REFRESHING_MAP" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Refreshing map..." + } + } + } }, "RETRY_BUTTON_LABEL" : { "localizations" : { @@ -394,8 +429,15 @@ } } }, - "Saving survey..." : { - + "SAVING_SURVEY" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Saving survey..." + } + } + } }, "SCHEDULE_LIST_TITLE" : { "localizations" : { @@ -468,12 +510,6 @@ } } } - }, - "Survey already taken." : { - - }, - "Survey not available." : { - }, "SURVEY_NOT_AVAILABLE_MESSAGE" : { "localizations" : { @@ -485,6 +521,16 @@ } } }, + "SURVEY_TAKEN_NOTICE" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "You have already taken the latest survey." + } + } + } + }, "TASK_CONTEXT_ACTION_QUESTIONNAIRE" : { "localizations" : { "en" : { @@ -635,12 +681,6 @@ } } } - }, - "Yesterday's survey." : { - - }, - "You've already taken the latest available survey." : { - } }, "version" : "1.0" diff --git a/StrokeCog/StrokeCogStandard.swift b/StrokeCog/StrokeCogStandard.swift index ca1ae9d..659e970 100644 --- a/StrokeCog/StrokeCogStandard.swift +++ b/StrokeCog/StrokeCogStandard.swift @@ -27,15 +27,15 @@ actor StrokeCogStandard: Standard, EnvironmentAccessible, HealthKitConstraint, O case userNotAuthenticatedYet case invalidStudyID } - + private static var userCollection: CollectionReference { Firestore.firestore().collection("users") } - + @Dependency var accountStorage: FirestoreAccountStorage? - + @AccountReference var account: Account - + private let logger = Logger(subsystem: "StrokeCog", category: "Standard") @@ -44,7 +44,7 @@ actor StrokeCogStandard: Standard, EnvironmentAccessible, HealthKitConstraint, O guard let details = await account.details else { throw StrokeCogStandardError.userNotAuthenticatedYet } - + return Self.userCollection.document(details.accountId) } } @@ -54,19 +54,19 @@ actor StrokeCogStandard: Standard, EnvironmentAccessible, HealthKitConstraint, O guard let details = await account.details else { throw StrokeCogStandardError.userNotAuthenticatedYet } - + return Storage.storage().reference().child("users/\(details.accountId)") } } - - + + init() { if !FeatureFlags.disableFirebase { _accountStorage = Dependency(wrappedValue: FirestoreAccountStorage(storeIn: StrokeCogStandard.userCollection)) } } - - + + func add(sample: HKSample) async { do { try await healthKitDocument(id: sample.id).setData(from: sample.resource) @@ -85,7 +85,7 @@ actor StrokeCogStandard: Standard, EnvironmentAccessible, HealthKitConstraint, O func add(response: ModelsR4.QuestionnaireResponse) async { let id = response.identifier?.value?.value?.string ?? UUID().uuidString - + do { try await userDocumentReference .collection("QuestionnaireResponse") // Add all HealthKit sources in a /QuestionnaireResponse collection. @@ -151,7 +151,7 @@ actor StrokeCogStandard: Standard, EnvironmentAccessible, HealthKitConstraint, O return locations } - + func add(response: DailySurveyResponse) async throws { guard let details = await account.details else { @@ -172,6 +172,24 @@ actor StrokeCogStandard: Standard, EnvironmentAccessible, HealthKitConstraint, O .collection("surveys") .document(UUID().uuidString) .setData(from: response) + + // Update the user document with the latest survey date + try await userDocumentReference.setData([ + "latestSurveyDate": response.surveyDate ?? "" + ], merge: true) + } + + func getLatestSurveyDate() async -> String { + let document = try? await userDocumentReference.getDocument() + + if let data = document?.data(), let surveyDate = data["latestSurveyDate"] as? String { + // Update the latest survey date in UserDefaults + UserDefaults.standard.set(surveyDate, forKey: StorageKeys.lastSurveyDate) + + return surveyDate + } else { + return "" + } } @@ -180,7 +198,7 @@ actor StrokeCogStandard: Standard, EnvironmentAccessible, HealthKitConstraint, O .collection("HealthKit") // Add all HealthKit sources in a /HealthKit collection. .document(uuid.uuidString) // Set the document identifier to the UUID of the document. } - + func deletedAccount() async throws { // delete all user associated data do { @@ -248,35 +266,46 @@ actor StrokeCogStandard: Standard, EnvironmentAccessible, HealthKitConstraint, O logger.error("Could not store consent form: \(error)") } } - + + /// Update the user document with the user's study ID + func setStudyID(_ studyID: String) async { + do { + try await userDocumentReference.setData([ + "studyID": studyID + ], merge: true) + } catch { + logger.error("Unable to set Study ID: \(error)") + } + } + func create(_ identifier: AdditionalRecordId, _ details: SignupDetails) async throws { guard let accountStorage else { preconditionFailure("Account Storage was requested although not enabled in current configuration.") } try await accountStorage.create(identifier, details) } - + func load(_ identifier: AdditionalRecordId, _ keys: [any AccountKey.Type]) async throws -> PartialAccountDetails { guard let accountStorage else { preconditionFailure("Account Storage was requested although not enabled in current configuration.") } return try await accountStorage.load(identifier, keys) } - + func modify(_ identifier: AdditionalRecordId, _ modifications: AccountModifications) async throws { guard let accountStorage else { preconditionFailure("Account Storage was requested although not enabled in current configuration.") } try await accountStorage.modify(identifier, modifications) } - + func clear(_ identifier: AdditionalRecordId) async { guard let accountStorage else { preconditionFailure("Account Storage was requested although not enabled in current configuration.") } await accountStorage.clear(identifier) } - + func delete(_ identifier: AdditionalRecordId) async throws { guard let accountStorage else { preconditionFailure("Account Storage was requested although not enabled in current configuration.") diff --git a/StrokeCog/Survey/DailySurveyTaskView.swift b/StrokeCog/Survey/DailySurveyTaskView.swift index 4193c6c..e0fda0d 100644 --- a/StrokeCog/Survey/DailySurveyTaskView.swift +++ b/StrokeCog/Survey/DailySurveyTaskView.swift @@ -53,7 +53,7 @@ struct DailySurveyTaskView: View { private var savingSurveyView: some View { VStack { - Text("Saving survey...") + Text("SAVING_SURVEY") ProgressView() } .padding() @@ -66,9 +66,9 @@ struct DailySurveyTaskView: View { Image(systemName: "exclamationmark.triangle.fill") .resizable() .frame(width: 50, height: 50) - .accessibilityLabel("Survey already taken.") + .accessibilityLabel("SURVEY_TAKEN_NOTICE") - Text("You've already taken the latest available survey.") + Text("SURVEY_TAKEN_NOTICE") .font(.largeTitle) .padding() .multilineTextAlignment(.center) @@ -86,9 +86,9 @@ struct DailySurveyTaskView: View { Image(systemName: "exclamationmark.triangle.fill") .resizable() .frame(width: 50, height: 50) - .accessibilityLabel("Yesterday's survey.") + .accessibilityLabel("PREVIOUS_DAY_SURVEY_LABEL") - Text("Please note that you are about to take yesterday's survey.") + Text("PREVIOUS_DAY_SURVEY_NOTICE") .font(.largeTitle) .padding() .multilineTextAlignment(.center) @@ -106,7 +106,7 @@ struct DailySurveyTaskView: View { Image(systemName: "clock.fill") .resizable() .frame(width: 50, height: 50) - .accessibilityLabel("Survey not available.") + .accessibilityLabel("SURVEY_NOT_AVAILABLE_MESSAGE") Text("SURVEY_NOT_AVAILABLE_MESSAGE") .font(.largeTitle) @@ -178,7 +178,6 @@ struct DailySurveyTaskView: View { response.surveyDate = surveyDateString // swiftlint:disable legacy_objc_type - /// Each answer is coded as a number, with -1 representing a question that was skipped. if let socialInteractionQuestion = taskResult.stepResult(forStepIdentifier: "SocialInteractionQuestion"), let result = socialInteractionQuestion.firstResult as? ORKChoiceQuestionResult, let answer = result.choiceAnswers?.first as? NSNumber { From cc7676cd7b9b62804bb613fa6b9144010a77f3a4 Mon Sep 17 00:00:00 2001 From: Vishnu Ravi Date: Mon, 10 Jun 2024 16:13:19 -0400 Subject: [PATCH 3/3] Fix swiftlint error --- StrokeCog/Home.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/StrokeCog/Home.swift b/StrokeCog/Home.swift index fae42f5..9cccd35 100644 --- a/StrokeCog/Home.swift +++ b/StrokeCog/Home.swift @@ -10,7 +10,7 @@ import SpeziAccount import SwiftUI -struct HomeView: View { +struct HomeView: View { static var accountEnabled: Bool { !FeatureFlags.disableFirebase && !FeatureFlags.skipOnboarding }