From f45b4ed07229e6b023a7a5d049583eff6ebbbaf5 Mon Sep 17 00:00:00 2001 From: Paul Kraft Date: Tue, 3 Sep 2024 14:49:14 -0700 Subject: [PATCH 1/5] Adapt --- ENGAGEHF.xcodeproj/project.pbxproj | 8 +++- .../xcshareddata/swiftpm/Package.resolved | 16 ++++---- ENGAGEHF/Account/Account+Setup.swift | 25 ++++++++++++ ENGAGEHF/Account/AccountSetupSheet.swift | 14 ++++--- ENGAGEHF/Account/AccountSheet.swift | 39 ++----------------- ENGAGEHF/Onboarding/AccountOnboarding.swift | 14 ++++--- ENGAGEHF/Resources/Localizable.xcstrings | 1 + 7 files changed, 61 insertions(+), 56 deletions(-) create mode 100644 ENGAGEHF/Account/Account+Setup.swift diff --git a/ENGAGEHF.xcodeproj/project.pbxproj b/ENGAGEHF.xcodeproj/project.pbxproj index 7936f619..d508678b 100644 --- a/ENGAGEHF.xcodeproj/project.pbxproj +++ b/ENGAGEHF.xcodeproj/project.pbxproj @@ -202,6 +202,7 @@ 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 */; }; + 9B91EE902C87924E004FF608 /* Account+Setup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B91EE8F2C87924E004FF608 /* Account+Setup.swift */; }; A9623C042C17A3F500189BA1 /* SpeziOmron in Frameworks */ = {isa = PBXBuildFile; productRef = A9623C032C17A3F500189BA1 /* SpeziOmron */; }; A9623C082C18D03F00189BA1 /* AccountSetupSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9623C072C18D03F00189BA1 /* AccountSetupSheet.swift */; }; A96C56BB2C0DFFCE00D6A50B /* InvitationCodeModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = A96C56BA2C0DFFCE00D6A50B /* InvitationCodeModule.swift */; }; @@ -409,6 +410,7 @@ 653A256128338800005D4D48 /* VitalsGraphAggregationUnitTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VitalsGraphAggregationUnitTests.swift; sourceTree = ""; }; 653A256728338800005D4D48 /* ENGAGEHFUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ENGAGEHFUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 653A258928339462005D4D48 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 9B91EE8F2C87924E004FF608 /* Account+Setup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Account+Setup.swift"; sourceTree = ""; }; A9623C072C18D03F00189BA1 /* AccountSetupSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountSetupSheet.swift; sourceTree = ""; }; A96C56BA2C0DFFCE00D6A50B /* InvitationCodeModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvitationCodeModule.swift; sourceTree = ""; }; A9720E422ABB68CC00872D23 /* AccountSetupHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountSetupHeader.swift; sourceTree = ""; }; @@ -1078,6 +1080,7 @@ 4D8CE0FE2C7540C000560327 /* AdditionalAccountSections.swift */, A96C56BA2C0DFFCE00D6A50B /* InvitationCodeModule.swift */, 4DB624852C73F1BF00573807 /* NotificationSettingsView.swift */, + 9B91EE8F2C87924E004FF608 /* Account+Setup.swift */, ); path = Account; sourceTree = ""; @@ -1353,6 +1356,7 @@ 4D62BF3B2C6A9F330088C414 /* (null) in Sources */, 4DDFC78A2BFC1312002B07A1 /* MessageRow.swift in Sources */, 4D8402722C4EC0D400817495 /* MeasurementListHeader.swift in Sources */, + 9B91EE902C87924E004FF608 /* Account+Setup.swift in Sources */, 4D92B9F22C3C8D1E00ABCED7 /* MeasurementListSection.swift in Sources */, 4D34B3ED2C404C61006F0D40 /* VitalsUnit.swift in Sources */, 4D40A4CA2C5AF34D003DC813 /* EducationalVideoCard.swift in Sources */, @@ -1980,8 +1984,8 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/StanfordSpezi/SpeziAccount.git"; requirement = { - kind = upToNextMajorVersion; - minimumVersion = "2.0.0-beta.7"; + branch = "feature/async-throwing-setup"; + kind = branch; }; }; 2FE5DC6529EDD894004B9AB4 /* XCRemoteSwiftPackageReference "SpeziContact" */ = { diff --git a/ENGAGEHF.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ENGAGEHF.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 259ddd46..49c79660 100644 --- a/ENGAGEHF.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/ENGAGEHF.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -49,7 +49,7 @@ { "identity" : "fhirmodels", "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/FHIRModels.git", + "location" : "https://github.com/apple/FHIRModels", "state" : { "revision" : "861afd5816a98d38f86220eab2f812d76cad84a0", "version" : "0.5.0" @@ -114,8 +114,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/StanfordBDHG/HealthKitOnFHIR.git", "state" : { - "branch" : "fix/swiftlint-dependency", - "revision" : "4c4c4d7020016e01904e07763a39088e5a4e132c" + "revision" : "87a9257e6fa37407f3437e4a0bf21dd09a4ea7c5", + "version" : "0.2.11" } }, { @@ -195,8 +195,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/StanfordSpezi/SpeziAccount.git", "state" : { - "revision" : "7e78ee9e7e4df2071a18ac0fc722671ac1dcfac4", - "version" : "2.0.0-beta.7" + "branch" : "feature/async-throwing-setup", + "revision" : "75465b62d9db8f733354997239069cf0b5061926" } }, { @@ -310,7 +310,7 @@ { "identity" : "swift-argument-parser", "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-argument-parser.git", + "location" : "https://github.com/apple/swift-argument-parser", "state" : { "revision" : "41982a3656a71c768319979febd796c6fd111d5c", "version" : "1.5.0" @@ -339,8 +339,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-nio.git", "state" : { - "revision" : "4c4453b489cf76e6b3b0f300aba663eb78182fad", - "version" : "2.70.0" + "revision" : "9746cf80e29edfef2a39924a66731249223f42a3", + "version" : "2.72.0" } }, { diff --git a/ENGAGEHF/Account/Account+Setup.swift b/ENGAGEHF/Account/Account+Setup.swift new file mode 100644 index 00000000..ee852a8f --- /dev/null +++ b/ENGAGEHF/Account/Account+Setup.swift @@ -0,0 +1,25 @@ +// +// Account+Setup.swift +// ENGAGEHF +// +// Created by Paul Kraft on 03.09.2024. +// + +import FirebaseFunctions +import SpeziAccount + +extension Account { + func setup() async throws { + do { + print("Calling setupUser") + _ = try await Functions.functions().httpsCallable("setupUser").call() + print("Called setupUser") + try await Task.sleep(for: .seconds(20)) + throw CancellationError() + } catch { + print("Failed setupUser", error) + await removeUserDetails() + throw error + } + } +} diff --git a/ENGAGEHF/Account/AccountSetupSheet.swift b/ENGAGEHF/Account/AccountSetupSheet.swift index a17f19f9..418b3d2c 100644 --- a/ENGAGEHF/Account/AccountSetupSheet.swift +++ b/ENGAGEHF/Account/AccountSetupSheet.swift @@ -6,21 +6,21 @@ // SPDX-License-Identifier: MIT // +import FirebaseFunctions @_spi(TestingSupport) import SpeziAccount import SpeziOnboarding +import SpeziViews import SwiftUI private struct AccountInvitationCodeView: View { - @Environment(\.dismiss) private var dismiss - @Environment(Account.self) private var account - + @Environment(\.dismiss) private var dismiss var body: some View { InvitationCodeView() .onChange(of: account.signedIn, initial: true) { - if account.signedIn { + if account.signedIn && account.details?.isAnonymous == false { dismiss() } } @@ -29,17 +29,21 @@ private struct AccountInvitationCodeView: View { struct AccountSetupSheet: View { + @Environment(Account.self) private var account @Environment(\.dismiss) private var dismiss + + @State private var viewState = ViewState.idle var body: some View { OnboardingStack { AccountInvitationCodeView() // we need this indirection, otherwise the onChange doesn't trigger AccountSetup { _ in - dismiss() + try await account.setup() } header: { AccountSetupHeader() } } + .viewStateAlert(state: $viewState) } } diff --git a/ENGAGEHF/Account/AccountSheet.swift b/ENGAGEHF/Account/AccountSheet.swift index eb6639b3..483bc265 100644 --- a/ENGAGEHF/Account/AccountSheet.swift +++ b/ENGAGEHF/Account/AccountSheet.swift @@ -6,50 +6,17 @@ // SPDX-License-Identifier: MIT // +import FirebaseFunctions @_spi(TestingSupport) import SpeziAccount import SpeziLicense import SwiftUI struct AccountSheet: View { - @Environment(\.dismiss) var dismiss - - @Environment(Account.self) private var account - @Environment(\.accountRequired) var accountRequired - - @State var isInSetup = false - - var body: some View { NavigationStack { - ZStack { - if account.signedIn && !isInSetup { - AccountOverview(close: .showCloseButton, deletion: .disabled) { - AdditionalAccountSections() - } - } else { - AccountSetup { _ in - dismiss() // we just signed in, dismiss the account setup sheet - } header: { - AccountSetupHeader() - } - .onAppear { - isInSetup = true - } - .toolbar { - if !accountRequired { - closeButton - } - } - } - } - } - } - - var closeButton: some ToolbarContent { - ToolbarItem(placement: .cancellationAction) { - Button("CLOSE") { - dismiss() + AccountOverview(close: .showCloseButton, deletion: .disabled) { + AdditionalAccountSections() } } } diff --git a/ENGAGEHF/Onboarding/AccountOnboarding.swift b/ENGAGEHF/Onboarding/AccountOnboarding.swift index 07ce6555..51549086 100644 --- a/ENGAGEHF/Onboarding/AccountOnboarding.swift +++ b/ENGAGEHF/Onboarding/AccountOnboarding.swift @@ -6,8 +6,10 @@ // SPDX-License-Identifier: MIT // +import FirebaseFunctions @_spi(TestingSupport) import SpeziAccount import SpeziOnboarding +import SpeziViews import SwiftUI @@ -24,14 +26,15 @@ struct AccountOnboarding: View { .login } } + + @State private var viewState = ViewState.idle var body: some View { AccountSetup { _ in - 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. - onboardingNavigationPath.nextStep() - } + try await account.setup() + // 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. + onboardingNavigationPath.nextStep() } header: { AccountSetupHeader() } continue: { @@ -43,6 +46,7 @@ struct AccountOnboarding: View { ) } .preferredAccountSetupStyle(setupStyle) + .viewStateAlert(state: $viewState) } } diff --git a/ENGAGEHF/Resources/Localizable.xcstrings b/ENGAGEHF/Resources/Localizable.xcstrings index c66fefe9..e4206790 100644 --- a/ENGAGEHF/Resources/Localizable.xcstrings +++ b/ENGAGEHF/Resources/Localizable.xcstrings @@ -160,6 +160,7 @@ }, "CLOSE" : { "comment" : "MARK: General", + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { From ac079dfa80b5405da2e0242946193afbceef1caa Mon Sep 17 00:00:00 2001 From: Paul Kraft Date: Thu, 5 Sep 2024 15:28:33 -0700 Subject: [PATCH 2/5] Reverse changes to dependencies --- ENGAGEHF.xcodeproj/project.pbxproj | 20 +++-- .../xcshareddata/swiftpm/Package.resolved | 10 +-- ...+Setup.swift => Account+FinishSetup.swift} | 6 +- ENGAGEHF/Account/AccountSetupSheet.swift | 40 ++++++---- ENGAGEHF/ContentView.swift | 55 +++++++++++++ ENGAGEHF/ENGAGEHF.swift | 13 +-- ENGAGEHF/Home.swift | 13 --- ENGAGEHF/Onboarding/AccountFinish.swift | 79 +++++++++++++++++++ ENGAGEHF/Onboarding/AccountOnboarding.swift | 9 ++- .../Onboarding/NotificationPermissions.swift | 9 +-- ENGAGEHF/Onboarding/OnboardingFlow.swift | 1 + ENGAGEHF/Resources/Localizable.xcstrings | 18 +++++ 12 files changed, 203 insertions(+), 70 deletions(-) rename ENGAGEHF/Account/{Account+Setup.swift => Account+FinishSetup.swift} (66%) create mode 100644 ENGAGEHF/ContentView.swift create mode 100644 ENGAGEHF/Onboarding/AccountFinish.swift diff --git a/ENGAGEHF.xcodeproj/project.pbxproj b/ENGAGEHF.xcodeproj/project.pbxproj index d508678b..382858e7 100644 --- a/ENGAGEHF.xcodeproj/project.pbxproj +++ b/ENGAGEHF.xcodeproj/project.pbxproj @@ -202,7 +202,9 @@ 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 */; }; - 9B91EE902C87924E004FF608 /* Account+Setup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B91EE8F2C87924E004FF608 /* Account+Setup.swift */; }; + 9B2401932C88FC1D003584BE /* AccountFinish.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B2401922C88FC1D003584BE /* AccountFinish.swift */; }; + 9B2401962C88FDC6003584BE /* Account+FinishSetup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B2401952C88FDC6003584BE /* Account+FinishSetup.swift */; }; + 9B9C21E02C89389900FE1430 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B9C21DF2C89389900FE1430 /* ContentView.swift */; }; A9623C042C17A3F500189BA1 /* SpeziOmron in Frameworks */ = {isa = PBXBuildFile; productRef = A9623C032C17A3F500189BA1 /* SpeziOmron */; }; A9623C082C18D03F00189BA1 /* AccountSetupSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9623C072C18D03F00189BA1 /* AccountSetupSheet.swift */; }; A96C56BB2C0DFFCE00D6A50B /* InvitationCodeModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = A96C56BA2C0DFFCE00D6A50B /* InvitationCodeModule.swift */; }; @@ -410,7 +412,9 @@ 653A256128338800005D4D48 /* VitalsGraphAggregationUnitTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VitalsGraphAggregationUnitTests.swift; sourceTree = ""; }; 653A256728338800005D4D48 /* ENGAGEHFUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ENGAGEHFUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 653A258928339462005D4D48 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 9B91EE8F2C87924E004FF608 /* Account+Setup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Account+Setup.swift"; sourceTree = ""; }; + 9B2401922C88FC1D003584BE /* AccountFinish.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountFinish.swift; sourceTree = ""; }; + 9B2401952C88FDC6003584BE /* Account+FinishSetup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Account+FinishSetup.swift"; sourceTree = ""; }; + 9B9C21DF2C89389900FE1430 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; A9623C072C18D03F00189BA1 /* AccountSetupSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountSetupSheet.swift; sourceTree = ""; }; A96C56BA2C0DFFCE00D6A50B /* InvitationCodeModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvitationCodeModule.swift; sourceTree = ""; }; A9720E422ABB68CC00872D23 /* AccountSetupHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountSetupHeader.swift; sourceTree = ""; }; @@ -498,6 +502,7 @@ 2FE5DC3429EDD7CA004B9AB4 /* Welcome.swift */, 2FE5DC3229EDD7CA004B9AB4 /* InterestingModules.swift */, 2FE5DCAC29EE6107004B9AB4 /* AccountOnboarding.swift */, + 9B2401922C88FC1D003584BE /* AccountFinish.swift */, 2F65B44D2A3B8B0600A36932 /* NotificationPermissions.swift */, 4DBDD3432BBFAD64001FB0CA /* InvitationCodeError.swift */, 4DBDD3452BBFAE2D001FB0CA /* InvitationCodeView.swift */, @@ -1011,6 +1016,7 @@ 2F5E32BC297E05EA003432F8 /* ENGAGEHFDelegate.swift */, 2FF53D8C2A8729D600042B76 /* ENGAGEHFStandard.swift */, 2F4E23822989D51F0013F3D9 /* ENGAGEHFTestingSetup.swift */, + 9B9C21DF2C89389900FE1430 /* ContentView.swift */, 2FC975A72978F11A00BA99FE /* Home.swift */, 4D9D43542C596CED000062D3 /* SharedExtensions */, 4D065BDE2C093FF600EBB3AE /* ReusableElements */, @@ -1080,7 +1086,7 @@ 4D8CE0FE2C7540C000560327 /* AdditionalAccountSections.swift */, A96C56BA2C0DFFCE00D6A50B /* InvitationCodeModule.swift */, 4DB624852C73F1BF00573807 /* NotificationSettingsView.swift */, - 9B91EE8F2C87924E004FF608 /* Account+Setup.swift */, + 9B2401952C88FDC6003584BE /* Account+FinishSetup.swift */, ); path = Account; sourceTree = ""; @@ -1297,6 +1303,7 @@ 4DF506192C2E1AAE003E7EFB /* SymptomsType.swift in Sources */, 4DF5062F2C2F2B00003E7EFB /* SymptomsPicker.swift in Sources */, 4D8AD2C52C4A180300CB4F3E /* MeasurementSeries.swift in Sources */, + 9B2401932C88FC1D003584BE /* AccountFinish.swift in Sources */, 4D8402C52C52C67100817495 /* RecommendationSummary.swift in Sources */, 4D92BA1A2C3E13F300ABCED7 /* FHIRSystem.swift in Sources */, 2FE5DC3A29EDD7CA004B9AB4 /* Welcome.swift in Sources */, @@ -1356,7 +1363,6 @@ 4D62BF3B2C6A9F330088C414 /* (null) in Sources */, 4DDFC78A2BFC1312002B07A1 /* MessageRow.swift in Sources */, 4D8402722C4EC0D400817495 /* MeasurementListHeader.swift in Sources */, - 9B91EE902C87924E004FF608 /* Account+Setup.swift in Sources */, 4D92B9F22C3C8D1E00ABCED7 /* MeasurementListSection.swift in Sources */, 4D34B3ED2C404C61006F0D40 /* VitalsUnit.swift in Sources */, 4D40A4CA2C5AF34D003DC813 /* EducationalVideoCard.swift in Sources */, @@ -1391,6 +1397,7 @@ 2F5E32BD297E05EA003432F8 /* ENGAGEHFDelegate.swift in Sources */, 4D065BE02C09401700EBB3AE /* StudyApplicationListCard.swift in Sources */, 4D9D43672C59A88C000062D3 /* ExpandableListCard.swift in Sources */, + 9B2401962C88FDC6003584BE /* Account+FinishSetup.swift in Sources */, 4D40A4BC2C5AB03E003DC813 /* ThumbnailView.swift in Sources */, 4D8AD2CD2C4ACFB700CB4F3E /* DateInterval+asAdjustedRange.swift in Sources */, 4D70CF6C2C65425F00EB4CC6 /* VitalsType.swift in Sources */, @@ -1398,6 +1405,7 @@ A96C56BB2C0DFFCE00D6A50B /* InvitationCodeModule.swift in Sources */, 4D4072762C45FF5B007C5621 /* GestureValue.swift in Sources */, 4DA20B5A2C25FEDE00715AA2 /* Decimal+IntConversion.swift in Sources */, + 9B9C21E02C89389900FE1430 /* ContentView.swift in Sources */, 4DF370082C66CAC9004D737A /* PDFViewer.swift in Sources */, 4D92B9F42C3C8DF900ABCED7 /* MeasurementListRow.swift in Sources */, 4D9D43742C59BDB2000062D3 /* (null) in Sources */, @@ -1984,8 +1992,8 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/StanfordSpezi/SpeziAccount.git"; requirement = { - branch = "feature/async-throwing-setup"; - kind = branch; + kind = upToNextMajorVersion; + minimumVersion = "2.0.0-beta.7"; }; }; 2FE5DC6529EDD894004B9AB4 /* XCRemoteSwiftPackageReference "SpeziContact" */ = { diff --git a/ENGAGEHF.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ENGAGEHF.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 49c79660..c926fc85 100644 --- a/ENGAGEHF.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/ENGAGEHF.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -49,7 +49,7 @@ { "identity" : "fhirmodels", "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/FHIRModels", + "location" : "https://github.com/apple/FHIRModels.git", "state" : { "revision" : "861afd5816a98d38f86220eab2f812d76cad84a0", "version" : "0.5.0" @@ -114,8 +114,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/StanfordBDHG/HealthKitOnFHIR.git", "state" : { - "revision" : "87a9257e6fa37407f3437e4a0bf21dd09a4ea7c5", - "version" : "0.2.11" + "branch" : "fix/swiftlint-dependency", + "revision" : "4c4c4d7020016e01904e07763a39088e5a4e132c" } }, { @@ -195,8 +195,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/StanfordSpezi/SpeziAccount.git", "state" : { - "branch" : "feature/async-throwing-setup", - "revision" : "75465b62d9db8f733354997239069cf0b5061926" + "revision" : "7e78ee9e7e4df2071a18ac0fc722671ac1dcfac4", + "version" : "2.0.0-beta.7" } }, { diff --git a/ENGAGEHF/Account/Account+Setup.swift b/ENGAGEHF/Account/Account+FinishSetup.swift similarity index 66% rename from ENGAGEHF/Account/Account+Setup.swift rename to ENGAGEHF/Account/Account+FinishSetup.swift index ee852a8f..6c535d65 100644 --- a/ENGAGEHF/Account/Account+Setup.swift +++ b/ENGAGEHF/Account/Account+FinishSetup.swift @@ -9,13 +9,9 @@ import FirebaseFunctions import SpeziAccount extension Account { - func setup() async throws { + func finishSetupIfNeeded() async throws { do { - print("Calling setupUser") _ = try await Functions.functions().httpsCallable("setupUser").call() - print("Called setupUser") - try await Task.sleep(for: .seconds(20)) - throw CancellationError() } catch { print("Failed setupUser", error) await removeUserDetails() diff --git a/ENGAGEHF/Account/AccountSetupSheet.swift b/ENGAGEHF/Account/AccountSetupSheet.swift index 418b3d2c..81152e93 100644 --- a/ENGAGEHF/Account/AccountSetupSheet.swift +++ b/ENGAGEHF/Account/AccountSetupSheet.swift @@ -15,35 +15,41 @@ import SwiftUI private struct AccountInvitationCodeView: View { @Environment(Account.self) private var account - @Environment(\.dismiss) private var dismiss var body: some View { InvitationCodeView() - .onChange(of: account.signedIn, initial: true) { - if account.signedIn && account.details?.isAnonymous == false { - dismiss() - } - } } } struct AccountSetupSheet: View { - @Environment(Account.self) private var account - @Environment(\.dismiss) private var dismiss - - @State private var viewState = ViewState.idle - - var body: some View { - OnboardingStack { - AccountInvitationCodeView() // we need this indirection, otherwise the onChange doesn't trigger + struct AccountOnboarding: View { + @Environment(OnboardingNavigationPath.self) private var onboardingNavigationPath + + var body: some View { AccountSetup { _ in - try await account.setup() + onboardingNavigationPath.nextStep() } header: { AccountSetupHeader() } } - .viewStateAlert(state: $viewState) + } + + @Binding private var isLoginActive: Bool + + var body: some View { + OnboardingStack(onboardingFlowComplete: !$isLoginActive) { + AccountInvitationCodeView() // we need this indirection, otherwise the onChange doesn't trigger + AccountOnboarding() + .onAppear { isLoginActive = true } + AccountFinish() + } + .onAppear { isLoginActive = false } + .interactiveDismissDisabled(isLoginActive) + } + + init(isLoginActive: Binding) { + self._isLoginActive = isLoginActive } } @@ -52,7 +58,7 @@ struct AccountSetupSheet: View { #Preview { Text(verbatim: "Base View") .sheet(isPresented: .constant(true)) { - AccountSetupSheet() + AccountSetupSheet(isLoginActive: .constant(true)) } .previewWith { AccountConfiguration(service: InMemoryAccountService()) diff --git a/ENGAGEHF/ContentView.swift b/ENGAGEHF/ContentView.swift new file mode 100644 index 00000000..45aa89b8 --- /dev/null +++ b/ENGAGEHF/ContentView.swift @@ -0,0 +1,55 @@ +// +// ContentView.swift +// ENGAGEHF +// +// Created by Paul Kraft on 04.09.2024. +// + +@_spi(TestingSupport) import SpeziAccount +import SpeziViews +import SwiftUI + +@MainActor +struct ContentView: View { + @AppStorage(StorageKeys.onboardingFlowComplete) var completedOnboardingFlow = false + @Environment(Account.self) private var account: Account + @State private var isLoginActive = false + @State private var isPresentingLogin = false + + private var shouldPresentLogin: Bool { + guard !FeatureFlags.disableFirebase + && !FeatureFlags.skipOnboarding + && completedOnboardingFlow else { + return false + } + return !account.signedIn + || (account.details?.isAnonymous ?? true) + || isLoginActive + } + + var body: some View { + ZStack { + if !completedOnboardingFlow { + EmptyView() + } else if isPresentingLogin { + EmptyView() + } else { + HomeView() + } + } + .onChange(of: shouldPresentLogin, initial: true) { + Task { @MainActor in + // This task makes sure to show the sheet slightly after the view, + // so that the presentation will actually occur - otherwise it might + // not happen + isPresentingLogin = shouldPresentLogin + } + } + .sheet(isPresented: !$completedOnboardingFlow) { + OnboardingFlow() + } + .sheet(isPresented: $isPresentingLogin) { + AccountSetupSheet(isLoginActive: $isLoginActive) + } + } +} diff --git a/ENGAGEHF/ENGAGEHF.swift b/ENGAGEHF/ENGAGEHF.swift index 02001141..22e17b3e 100644 --- a/ENGAGEHF/ENGAGEHF.swift +++ b/ENGAGEHF/ENGAGEHF.swift @@ -21,21 +21,10 @@ struct ENGAGEHF: App { @UIApplicationDelegateAdaptor(ENGAGEHFDelegate.self) var appDelegate - @AppStorage(StorageKeys.onboardingFlowComplete) var completedOnboardingFlow = false - var body: some Scene { WindowGroup { - ZStack { - if completedOnboardingFlow { - HomeView() - } else { - EmptyView() - } - } - .sheet(isPresented: !$completedOnboardingFlow) { - OnboardingFlow() - } + ContentView() .testingSetup() .spezi(appDelegate) } diff --git a/ENGAGEHF/Home.swift b/ENGAGEHF/Home.swift index f0563d64..9d1a6b35 100644 --- a/ENGAGEHF/Home.swift +++ b/ENGAGEHF/Home.swift @@ -22,23 +22,13 @@ struct HomeView: View { case heart case medications case education - case devices - } - - - // Disable bluetooth in preview to prevent preview from crashing - private var bluetoothEnabled: Bool { - !ProcessInfo.processInfo.isPreviewSimulator } @Environment(HealthMeasurements.self) private var measurements - @Environment(Bluetooth.self) private var bluetooth @Environment(ENGAGEHFStandard.self) private var standard @Environment(NavigationManager.self) private var navigationManager @Environment(NotificationManager.self) private var notificationManager - - @Environment(\.dismiss) private var dismiss @State private var presentingAccount = false @@ -79,9 +69,6 @@ struct HomeView: View { .sheet(isPresented: $presentingAccount) { AccountSheet() } - .accountRequired(!FeatureFlags.disableFirebase && !FeatureFlags.skipOnboarding) { - AccountSetupSheet() - } .sheet(isPresented: $measurements.shouldPresentMeasurements) { MeasurementsRecordedSheet { samples in try await standard.addMeasurement(samples: samples) diff --git a/ENGAGEHF/Onboarding/AccountFinish.swift b/ENGAGEHF/Onboarding/AccountFinish.swift new file mode 100644 index 00000000..931d043f --- /dev/null +++ b/ENGAGEHF/Onboarding/AccountFinish.swift @@ -0,0 +1,79 @@ +// +// 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 +// + +@_spi(TestingSupport) import SpeziAccount +import SpeziOnboarding +import SpeziViews +import SwiftUI + + +struct AccountFinish: View { + @Environment(Account.self) private var account + @Environment(OnboardingNavigationPath.self) private var onboardingNavigationPath + + @State private var viewState = ViewState.processing + + var body: some View { + VStack { + switch viewState { + case .idle: + Text("Account Creation Finished!") + .font(.title) + Text("Your account has successfully been created. You may now proceed.") + .font(.headline) + + Button("Continue") { + onboardingNavigationPath.nextStep() + } + case .processing: + ProgressView() + case let .error(error): + Text("Error occured: \(error.errorDescription ?? "")") + + Button("Try again") { + onboardingNavigationPath.removeLast() + } + } + } + .task { + do { + try await account.finishSetupIfNeeded() + viewState = .idle + } catch { + viewState = .error(AnyLocalizedError(error: error)) + } + } + .navigationTitle(Text("Finishing Account Setup")) + .navigationBarBackButtonHidden(true) + } +} + + +#if DEBUG +#Preview("Account Finish SignIn") { + OnboardingStack { + AccountFinish() + } + .previewWith { + AccountConfiguration(service: InMemoryAccountService()) + } +} + +#Preview("Account Finish SignUp") { + var details = AccountDetails() + details.userId = "lelandstanford@stanford.edu" + details.name = PersonNameComponents(givenName: "Leland", familyName: "Stanford") + + return OnboardingStack { + AccountFinish() + } + .previewWith { + AccountConfiguration(service: InMemoryAccountService(), activeDetails: details) + } +} +#endif diff --git a/ENGAGEHF/Onboarding/AccountOnboarding.swift b/ENGAGEHF/Onboarding/AccountOnboarding.swift index 51549086..edc222b6 100644 --- a/ENGAGEHF/Onboarding/AccountOnboarding.swift +++ b/ENGAGEHF/Onboarding/AccountOnboarding.swift @@ -31,10 +31,11 @@ struct AccountOnboarding: View { var body: some View { AccountSetup { _ in - try await account.setup() - // 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. - onboardingNavigationPath.nextStep() + 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. + onboardingNavigationPath.nextStep() + } } header: { AccountSetupHeader() } continue: { diff --git a/ENGAGEHF/Onboarding/NotificationPermissions.swift b/ENGAGEHF/Onboarding/NotificationPermissions.swift index 75848ff1..bc869ee8 100644 --- a/ENGAGEHF/Onboarding/NotificationPermissions.swift +++ b/ENGAGEHF/Onboarding/NotificationPermissions.swift @@ -15,9 +15,6 @@ struct NotificationPermissions: View { @Environment(OnboardingNavigationPath.self) private var onboardingNavigationPath @Environment(NotificationManager.self) private var notificationManager - @State private var notificationProcessing = false - - var body: some View { OnboardingView( contentView: { @@ -40,16 +37,12 @@ struct NotificationPermissions: View { OnboardingActionsView( primaryText: "NOTIFICATION_PERMISSIONS_BUTTON", primaryAction: { - notificationProcessing = true - // Notification Authorization is not available in the preview simulator. if ProcessInfo.processInfo.isPreviewSimulator { try await _Concurrency.Task.sleep(for: .seconds(5)) } else { _ = try await notificationManager.requestNotificationPermissions() } - - notificationProcessing = false onboardingNavigationPath.nextStep() }, secondaryText: "Skip", @@ -59,7 +52,7 @@ struct NotificationPermissions: View { ) } ) - .navigationBarBackButtonHidden(notificationProcessing) + .navigationBarBackButtonHidden(true) // Small fix as otherwise "Login" or "Sign up" is still shown in the nav bar .navigationTitle(Text(verbatim: "")) } diff --git a/ENGAGEHF/Onboarding/OnboardingFlow.swift b/ENGAGEHF/Onboarding/OnboardingFlow.swift index 277c3e25..3235ecd0 100644 --- a/ENGAGEHF/Onboarding/OnboardingFlow.swift +++ b/ENGAGEHF/Onboarding/OnboardingFlow.swift @@ -25,6 +25,7 @@ struct OnboardingFlow: View { if !FeatureFlags.disableFirebase { InvitationCodeView() AccountOnboarding() + AccountFinish() } NotificationPermissions() diff --git a/ENGAGEHF/Resources/Localizable.xcstrings b/ENGAGEHF/Resources/Localizable.xcstrings index e4206790..1182f488 100644 --- a/ENGAGEHF/Resources/Localizable.xcstrings +++ b/ENGAGEHF/Resources/Localizable.xcstrings @@ -45,6 +45,9 @@ }, "About %@" : { + }, + "Account Creation Finished!" : { + }, "ACCOUNT_NEXT" : { "localizations" : { @@ -185,6 +188,9 @@ }, "Content ..." : { + }, + "Continue" : { + }, "Current" : { @@ -209,6 +215,9 @@ }, "Enable Notifications in Settings" : { + }, + "Error occured: %@" : { + }, "Expansion Button" : { @@ -224,6 +233,9 @@ }, "Failed to save new %@ measurement." : { + }, + "Finishing Account Setup" : { + }, "Generating Health Summary" : { @@ -673,6 +685,9 @@ }, "Trigger Weight Measurement" : { + }, + "Try again" : { + }, "Unable to create time interval for date: %@" : { @@ -846,6 +861,9 @@ } } } + }, + "Your account has successfully been created. You may now proceed." : { + } }, "version" : "1.0" From 13957beb20df6a60a014674895f0884f0b49950b Mon Sep 17 00:00:00 2001 From: Paul Kraft Date: Thu, 5 Sep 2024 15:30:31 -0700 Subject: [PATCH 3/5] Reverse changes to package.resolved --- .../xcshareddata/swiftpm/Package.resolved | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ENGAGEHF.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ENGAGEHF.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index c926fc85..35cad30e 100644 --- a/ENGAGEHF.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/ENGAGEHF.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -49,7 +49,7 @@ { "identity" : "fhirmodels", "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/FHIRModels.git", + "location" : "https://github.com/apple/FHIRModels", "state" : { "revision" : "861afd5816a98d38f86220eab2f812d76cad84a0", "version" : "0.5.0" @@ -114,8 +114,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/StanfordBDHG/HealthKitOnFHIR.git", "state" : { - "branch" : "fix/swiftlint-dependency", - "revision" : "4c4c4d7020016e01904e07763a39088e5a4e132c" + "revision" : "87a9257e6fa37407f3437e4a0bf21dd09a4ea7c5", + "version" : "0.2.11" } }, { From 0a1c2304bfbcf1143534c415465a1705b2f5c0e0 Mon Sep 17 00:00:00 2001 From: Paul Kraft Date: Thu, 12 Sep 2024 10:47:46 -0700 Subject: [PATCH 4/5] Rewrite auth flow, get organization id and notification settings from Account --- ENGAGEHF.xcodeproj/project.pbxproj | 48 +++---- .../xcshareddata/swiftpm/Package.resolved | 8 +- ENGAGEHF/Account/Account+FinishSetup.swift | 21 --- ENGAGEHF/Account/AccountSetupSheet.swift | 68 ---------- ENGAGEHF/Account/InvitationCodeModule.swift | 20 +-- .../Account/NotificationSettingsView.swift | 92 ++++++++----- ENGAGEHF/ContentView.swift | 110 ++++++++++----- ENGAGEHF/ENGAGEHFDelegate.swift | 16 ++- .../MedicationsManager.swift | 7 +- .../MessageManager/MessageManager.swift | 7 +- .../NavigationManager/NavigationManager.swift | 33 ++--- .../NotificationManager.swift | 19 +-- .../NotificationSettings.swift | 65 --------- ...onInformation.swift => Organization.swift} | 11 +- .../UserMetaDataManager.swift | 126 +++--------------- .../Managers/VideoManager/VideoManager.swift | 3 +- .../VitalsManager/VitalsManager.swift | 11 +- ENGAGEHF/Onboarding/AccountFinish.swift | 79 ----------- ENGAGEHF/Onboarding/AccountOnboarding.swift | 26 ++-- ENGAGEHF/Onboarding/AuthFlow.swift | 19 +++ ENGAGEHF/Onboarding/InvitationCodeView.swift | 56 ++++---- ENGAGEHF/Onboarding/OnboardingFlow.swift | 6 - ENGAGEHF/Resources/Localizable.xcstrings | 31 ++--- .../SharedExtensions/Account+Binding.swift | 52 ++++++++ .../SharedExtensions/AccountDetails+Key.swift | 106 +++++++++++++++ .../AccountNotifications+Extras.swift | 33 +++++ 26 files changed, 494 insertions(+), 579 deletions(-) delete mode 100644 ENGAGEHF/Account/Account+FinishSetup.swift delete mode 100644 ENGAGEHF/Account/AccountSetupSheet.swift delete mode 100644 ENGAGEHF/Managers/UserMetaDataManager/NotificationSettings.swift rename ENGAGEHF/Managers/UserMetaDataManager/{OrganizationInformation.swift => Organization.swift} (82%) delete mode 100644 ENGAGEHF/Onboarding/AccountFinish.swift create mode 100644 ENGAGEHF/Onboarding/AuthFlow.swift create mode 100644 ENGAGEHF/SharedExtensions/Account+Binding.swift create mode 100644 ENGAGEHF/SharedExtensions/AccountDetails+Key.swift create mode 100644 ENGAGEHF/SharedExtensions/AccountNotifications+Extras.swift diff --git a/ENGAGEHF.xcodeproj/project.pbxproj b/ENGAGEHF.xcodeproj/project.pbxproj index 382858e7..52cb1632 100644 --- a/ENGAGEHF.xcodeproj/project.pbxproj +++ b/ENGAGEHF.xcodeproj/project.pbxproj @@ -44,8 +44,7 @@ 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 */; }; - 4D081A1F2C6C1A3400E13BC8 /* OrganizationInformation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D081A1E2C6C1A3400E13BC8 /* OrganizationInformation.swift */; }; - 4D081A262C6C2C6B00E13BC8 /* NotificationSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D081A252C6C2C6B00E13BC8 /* NotificationSettings.swift */; }; + 4D081A1F2C6C1A3400E13BC8 /* Organization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D081A1E2C6C1A3400E13BC8 /* Organization.swift */; }; 4D081A282C6C2C8400E13BC8 /* UserMetaDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D081A272C6C2C8400E13BC8 /* UserMetaDataManager.swift */; }; 4D0F389E2C09779100DA12F7 /* ShowMoreButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D0F389D2C09779000DA12F7 /* ShowMoreButton.swift */; }; 4D0F38A22C0A7AD900DA12F7 /* ExpandableText.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D0F38A12C0A7AD900DA12F7 /* ExpandableText.swift */; }; @@ -202,11 +201,12 @@ 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 */; }; - 9B2401932C88FC1D003584BE /* AccountFinish.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B2401922C88FC1D003584BE /* AccountFinish.swift */; }; - 9B2401962C88FDC6003584BE /* Account+FinishSetup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B2401952C88FDC6003584BE /* Account+FinishSetup.swift */; }; + 9B1864882C9226E90042AC81 /* AccountDetails+Key.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B1864872C9226E90042AC81 /* AccountDetails+Key.swift */; }; + 9B18648B2C9227040042AC81 /* AccountNotifications+Extras.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B18648A2C9227040042AC81 /* AccountNotifications+Extras.swift */; }; + 9B1864942C9276C40042AC81 /* AuthFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B1864932C9276C40042AC81 /* AuthFlow.swift */; }; + 9B1864982C9350D30042AC81 /* Account+Binding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B1864972C9350D30042AC81 /* Account+Binding.swift */; }; 9B9C21E02C89389900FE1430 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B9C21DF2C89389900FE1430 /* ContentView.swift */; }; A9623C042C17A3F500189BA1 /* SpeziOmron in Frameworks */ = {isa = PBXBuildFile; productRef = A9623C032C17A3F500189BA1 /* SpeziOmron */; }; - A9623C082C18D03F00189BA1 /* AccountSetupSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9623C072C18D03F00189BA1 /* AccountSetupSheet.swift */; }; A96C56BB2C0DFFCE00D6A50B /* InvitationCodeModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = A96C56BA2C0DFFCE00D6A50B /* InvitationCodeModule.swift */; }; A9720E432ABB68CC00872D23 /* AccountSetupHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9720E422ABB68CC00872D23 /* AccountSetupHeader.swift */; }; A977DD542C25E2DA00A2A8E5 /* SpeziDevices in Frameworks */ = {isa = PBXBuildFile; productRef = A977DD532C25E2DA00A2A8E5 /* SpeziDevices */; }; @@ -260,8 +260,7 @@ 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 = ""; }; - 4D081A1E2C6C1A3400E13BC8 /* OrganizationInformation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrganizationInformation.swift; sourceTree = ""; }; - 4D081A252C6C2C6B00E13BC8 /* NotificationSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettings.swift; sourceTree = ""; }; + 4D081A1E2C6C1A3400E13BC8 /* Organization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Organization.swift; sourceTree = ""; }; 4D081A272C6C2C8400E13BC8 /* UserMetaDataManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserMetaDataManager.swift; sourceTree = ""; }; 4D0F389D2C09779000DA12F7 /* ShowMoreButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShowMoreButton.swift; sourceTree = ""; }; 4D0F38A12C0A7AD900DA12F7 /* ExpandableText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExpandableText.swift; sourceTree = ""; }; @@ -412,10 +411,11 @@ 653A256128338800005D4D48 /* VitalsGraphAggregationUnitTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VitalsGraphAggregationUnitTests.swift; sourceTree = ""; }; 653A256728338800005D4D48 /* ENGAGEHFUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ENGAGEHFUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 653A258928339462005D4D48 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 9B2401922C88FC1D003584BE /* AccountFinish.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountFinish.swift; sourceTree = ""; }; - 9B2401952C88FDC6003584BE /* Account+FinishSetup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Account+FinishSetup.swift"; sourceTree = ""; }; + 9B1864872C9226E90042AC81 /* AccountDetails+Key.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AccountDetails+Key.swift"; sourceTree = ""; }; + 9B18648A2C9227040042AC81 /* AccountNotifications+Extras.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AccountNotifications+Extras.swift"; sourceTree = ""; }; + 9B1864932C9276C40042AC81 /* AuthFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthFlow.swift; sourceTree = ""; }; + 9B1864972C9350D30042AC81 /* Account+Binding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Account+Binding.swift"; sourceTree = ""; }; 9B9C21DF2C89389900FE1430 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; - A9623C072C18D03F00189BA1 /* AccountSetupSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountSetupSheet.swift; sourceTree = ""; }; A96C56BA2C0DFFCE00D6A50B /* InvitationCodeModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvitationCodeModule.swift; sourceTree = ""; }; A9720E422ABB68CC00872D23 /* AccountSetupHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountSetupHeader.swift; sourceTree = ""; }; A9DFE8A82ABE551400428242 /* AccountButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountButton.swift; sourceTree = ""; }; @@ -499,10 +499,10 @@ isa = PBXGroup; children = ( 2FE5DC3129EDD7CA004B9AB4 /* OnboardingFlow.swift */, + 9B1864932C9276C40042AC81 /* AuthFlow.swift */, 2FE5DC3429EDD7CA004B9AB4 /* Welcome.swift */, 2FE5DC3229EDD7CA004B9AB4 /* InterestingModules.swift */, 2FE5DCAC29EE6107004B9AB4 /* AccountOnboarding.swift */, - 9B2401922C88FC1D003584BE /* AccountFinish.swift */, 2F65B44D2A3B8B0600A36932 /* NotificationPermissions.swift */, 4DBDD3432BBFAD64001FB0CA /* InvitationCodeError.swift */, 4DBDD3452BBFAE2D001FB0CA /* InvitationCodeView.swift */, @@ -573,8 +573,7 @@ 4D081A242C6C2C4500E13BC8 /* UserMetaDataManager */ = { isa = PBXGroup; children = ( - 4D081A1E2C6C1A3400E13BC8 /* OrganizationInformation.swift */, - 4D081A252C6C2C6B00E13BC8 /* NotificationSettings.swift */, + 4D081A1E2C6C1A3400E13BC8 /* Organization.swift */, 4D081A272C6C2C8400E13BC8 /* UserMetaDataManager.swift */, ); path = UserMetaDataManager; @@ -863,6 +862,9 @@ 4D70CF642C653BE100EB4CC6 /* Image+CardSymbolStyle.swift */, 4DF36FF82C66A884004D737A /* String+Identifiable.swift */, 4D62BF3F2C6AB8520088C414 /* KeyedDecodingContainer+DecodeISO8601Date.swift */, + 9B1864872C9226E90042AC81 /* AccountDetails+Key.swift */, + 9B18648A2C9227040042AC81 /* AccountNotifications+Extras.swift */, + 9B1864972C9350D30042AC81 /* Account+Binding.swift */, ); path = SharedExtensions; sourceTree = ""; @@ -1081,12 +1083,10 @@ children = ( A9DFE8A82ABE551400428242 /* AccountButton.swift */, A9720E422ABB68CC00872D23 /* AccountSetupHeader.swift */, - A9623C072C18D03F00189BA1 /* AccountSetupSheet.swift */, A9FE7ACF2AA39BAB0077B045 /* AccountSheet.swift */, 4D8CE0FE2C7540C000560327 /* AdditionalAccountSections.swift */, A96C56BA2C0DFFCE00D6A50B /* InvitationCodeModule.swift */, 4DB624852C73F1BF00573807 /* NotificationSettingsView.swift */, - 9B2401952C88FDC6003584BE /* Account+FinishSetup.swift */, ); path = Account; sourceTree = ""; @@ -1288,7 +1288,7 @@ 2FE5DC4129EDD7EE004B9AB4 /* StorageKeys.swift in Sources */, 4D73A51B2C581C8F00CCEC46 /* TargetLabelSizeKey.swift in Sources */, 4D0F389E2C09779100DA12F7 /* ShowMoreButton.swift in Sources */, - 4D081A1F2C6C1A3400E13BC8 /* OrganizationInformation.swift in Sources */, + 4D081A1F2C6C1A3400E13BC8 /* Organization.swift in Sources */, 4DDFC7652BF350C2002B07A1 /* Education.swift in Sources */, 4DBE86DD2C7F9BBF0003390B /* MobilePlatform.swift in Sources */, 4D70CF6A2C65423D00EB4CC6 /* GraphSelection.swift in Sources */, @@ -1303,7 +1303,6 @@ 4DF506192C2E1AAE003E7EFB /* SymptomsType.swift in Sources */, 4DF5062F2C2F2B00003E7EFB /* SymptomsPicker.swift in Sources */, 4D8AD2C52C4A180300CB4F3E /* MeasurementSeries.swift in Sources */, - 9B2401932C88FC1D003584BE /* AccountFinish.swift in Sources */, 4D8402C52C52C67100817495 /* RecommendationSummary.swift in Sources */, 4D92BA1A2C3E13F300ABCED7 /* FHIRSystem.swift in Sources */, 2FE5DC3A29EDD7CA004B9AB4 /* Welcome.swift in Sources */, @@ -1315,6 +1314,7 @@ 4D8CE0FC2C753A4700560327 /* NotificationManager.swift in Sources */, 4DC0F67A2C34AD620025AB13 /* VitalsList.swift in Sources */, 2FE5DC3829EDD7CA004B9AB4 /* InterestingModules.swift in Sources */, + 9B1864882C9226E90042AC81 /* AccountDetails+Key.swift in Sources */, 4D8402B22C51D62400817495 /* MedicationsList.swift in Sources */, 4DC990612C7D3A13001E86C5 /* ClosedRange+InitFromSequence.swift in Sources */, 4D73A5112C58088E00CCEC46 /* PositionedCurrentLabel.swift in Sources */, @@ -1323,6 +1323,7 @@ 4DA20B642C2A0FF100715AA2 /* VitalsCard.swift in Sources */, 4DBDD3442BBFAD64001FB0CA /* InvitationCodeError.swift in Sources */, 4D8402B52C51D6D200817495 /* MedicationRecommendationSymbol.swift in Sources */, + 9B1864982C9350D30042AC81 /* Account+Binding.swift in Sources */, 4D40A4AF2C5A9B4F003DC813 /* VideoPlayer.swift in Sources */, 4D84027E2C4EF82D00817495 /* AddMeasurementAsyncButton.swift in Sources */, 4D8402AE2C51C66D00817495 /* Medications.swift in Sources */, @@ -1330,7 +1331,6 @@ 4DF3700D2C66E479004D737A /* PDFDocument+Transferable.swift in Sources */, 2FC975A82978F11A00BA99FE /* Home.swift in Sources */, 4D40A4C62C5AE486003DC813 /* VideoListSection.swift in Sources */, - A9623C082C18D03F00189BA1 /* AccountSetupSheet.swift in Sources */, 4D8CE0F62C752D1C00560327 /* VideoCollection.swift in Sources */, 4D40727E2C465554007C5621 /* HKSampleGraphError.swift in Sources */, 4D4072782C460014007C5621 /* TapDrag+GestureValue.swift in Sources */, @@ -1344,6 +1344,7 @@ 4D40729F2C488172007C5621 /* KnownVitalsSeries.swift in Sources */, 4D8CE0F52C752D1C00560327 /* Video.swift in Sources */, 4DF36FFD2C66A904004D737A /* QuestionnaireSheetView.swift in Sources */, + 9B18648B2C9227040042AC81 /* AccountNotifications+Extras.swift in Sources */, 2FF350E42C4F3F0B0095F08F /* Firebase+References.swift in Sources */, 4DC0F6712C34818B0025AB13 /* HKSample+GetDoubleValues.swift in Sources */, 4D62BF392C6A978C0088C414 /* (null) in Sources */, @@ -1367,7 +1368,6 @@ 4D34B3ED2C404C61006F0D40 /* VitalsUnit.swift in Sources */, 4D40A4CA2C5AF34D003DC813 /* EducationalVideoCard.swift in Sources */, 4D05F26D2C54696100201581 /* DosageGaugeStyle.swift in Sources */, - 4D081A262C6C2C6B00E13BC8 /* NotificationSettings.swift in Sources */, 4D73A5142C5816AF00CCEC46 /* View+ReadSize.swift in Sources */, 4D40A4D22C5B0AB2003DC813 /* VideoList.swift in Sources */, 4D4670CC2C79408C004DEAC4 /* VitalsGraph+GraphHeader.swift in Sources */, @@ -1397,7 +1397,6 @@ 2F5E32BD297E05EA003432F8 /* ENGAGEHFDelegate.swift in Sources */, 4D065BE02C09401700EBB3AE /* StudyApplicationListCard.swift in Sources */, 4D9D43672C59A88C000062D3 /* ExpandableListCard.swift in Sources */, - 9B2401962C88FDC6003584BE /* Account+FinishSetup.swift in Sources */, 4D40A4BC2C5AB03E003DC813 /* ThumbnailView.swift in Sources */, 4D8AD2CD2C4ACFB700CB4F3E /* DateInterval+asAdjustedRange.swift in Sources */, 4D70CF6C2C65425F00EB4CC6 /* VitalsType.swift in Sources */, @@ -1424,6 +1423,7 @@ 4D8402702C4EBE7D00817495 /* AddMeasurementView.swift in Sources */, 4D2D1D602C2B859C00ABDC19 /* GraphPicker.swift in Sources */, 4D8402DE2C5417F400817495 /* MedicationDescription.swift in Sources */, + 9B1864942C9276C40042AC81 /* AuthFlow.swift in Sources */, 4DBDD3462BBFAE2D001FB0CA /* InvitationCodeView.swift in Sources */, 4D40A4B62C5AA094003DC813 /* VideoView.swift in Sources */, 4DB624862C73F1BF00573807 /* NotificationSettingsView.swift in Sources */, @@ -1992,8 +1992,8 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/StanfordSpezi/SpeziAccount.git"; requirement = { - kind = upToNextMajorVersion; - minimumVersion = "2.0.0-beta.7"; + branch = "feature/async-throwing-setup"; + kind = branch; }; }; 2FE5DC6529EDD894004B9AB4 /* XCRemoteSwiftPackageReference "SpeziContact" */ = { @@ -2016,8 +2016,8 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/StanfordSpezi/SpeziFirebase.git"; requirement = { - kind = upToNextMajorVersion; - minimumVersion = "2.0.0-beta.3"; + branch = "fetch-account-details"; + 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 35cad30e..6d858150 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" : "7e78ee9e7e4df2071a18ac0fc722671ac1dcfac4", - "version" : "2.0.0-beta.7" + "branch" : "feature/async-throwing-setup", + "revision" : "63cf7b601020942bdce4c601f9b01d835863b65a" } }, { @@ -231,8 +231,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/StanfordSpezi/SpeziFirebase.git", "state" : { - "revision" : "d0cb58fc43c99e632446b43c5cb56943f8339bf2", - "version" : "2.0.0-beta.3" + "branch" : "fetch-account-details", + "revision" : "42e15c9332f317388e80dca61dd10f9a0bf66e5c" } }, { diff --git a/ENGAGEHF/Account/Account+FinishSetup.swift b/ENGAGEHF/Account/Account+FinishSetup.swift deleted file mode 100644 index 6c535d65..00000000 --- a/ENGAGEHF/Account/Account+FinishSetup.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// Account+Setup.swift -// ENGAGEHF -// -// Created by Paul Kraft on 03.09.2024. -// - -import FirebaseFunctions -import SpeziAccount - -extension Account { - func finishSetupIfNeeded() async throws { - do { - _ = try await Functions.functions().httpsCallable("setupUser").call() - } catch { - print("Failed setupUser", error) - await removeUserDetails() - throw error - } - } -} diff --git a/ENGAGEHF/Account/AccountSetupSheet.swift b/ENGAGEHF/Account/AccountSetupSheet.swift deleted file mode 100644 index 81152e93..00000000 --- a/ENGAGEHF/Account/AccountSetupSheet.swift +++ /dev/null @@ -1,68 +0,0 @@ -// -// 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 FirebaseFunctions -@_spi(TestingSupport) import SpeziAccount -import SpeziOnboarding -import SpeziViews -import SwiftUI - - -private struct AccountInvitationCodeView: View { - @Environment(Account.self) private var account - - var body: some View { - InvitationCodeView() - } -} - - -struct AccountSetupSheet: View { - struct AccountOnboarding: View { - @Environment(OnboardingNavigationPath.self) private var onboardingNavigationPath - - var body: some View { - AccountSetup { _ in - onboardingNavigationPath.nextStep() - } header: { - AccountSetupHeader() - } - } - } - - @Binding private var isLoginActive: Bool - - var body: some View { - OnboardingStack(onboardingFlowComplete: !$isLoginActive) { - AccountInvitationCodeView() // we need this indirection, otherwise the onChange doesn't trigger - AccountOnboarding() - .onAppear { isLoginActive = true } - AccountFinish() - } - .onAppear { isLoginActive = false } - .interactiveDismissDisabled(isLoginActive) - } - - init(isLoginActive: Binding) { - self._isLoginActive = isLoginActive - } -} - - -#if DEBUG -#Preview { - Text(verbatim: "Base View") - .sheet(isPresented: .constant(true)) { - AccountSetupSheet(isLoginActive: .constant(true)) - } - .previewWith { - AccountConfiguration(service: InMemoryAccountService()) - InvitationCodeModule() - } -} -#endif diff --git a/ENGAGEHF/Account/InvitationCodeModule.swift b/ENGAGEHF/Account/InvitationCodeModule.swift index 4d2718a3..3f532c61 100644 --- a/ENGAGEHF/Account/InvitationCodeModule.swift +++ b/ENGAGEHF/Account/InvitationCodeModule.swift @@ -51,26 +51,14 @@ class InvitationCodeModule: Module, EnvironmentAccessible { guard invitationCode == "ENGAGEHFTEST1" else { throw InvitationCodeError.invitationCodeInvalid } - + try? await Task.sleep(for: .seconds(0.25)) } else { - guard let accountService else { - preconditionFailure("The Firebase Account Service was not present even though `disableFirebase` was turned off!") - } - - try await signOutAccount() - try await accountService.signUpAnonymously() - - let checkInvitationCode = Functions.functions().httpsCallable("checkInvitationCode") - do { - _ = try await checkInvitationCode.call( - [ - "invitationCode": invitationCode - ] - ) + let enrollUser = Functions.functions().httpsCallable("enrollUser") + _ = try await enrollUser.call(["invitationCode": invitationCode]) } catch { - logger.error("Failed to check invitation code: \(error)") + logger.error("Failed to enroll user: \(error)") throw InvitationCodeError.invitationCodeInvalid } } diff --git a/ENGAGEHF/Account/NotificationSettingsView.swift b/ENGAGEHF/Account/NotificationSettingsView.swift index cd33886a..2f269261 100644 --- a/ENGAGEHF/Account/NotificationSettingsView.swift +++ b/ENGAGEHF/Account/NotificationSettingsView.swift @@ -10,63 +10,83 @@ import SpeziAccount import SpeziViews import SwiftUI - +@MainActor struct NotificationSettingsView: View { + @Environment(Account.self) private var account @Environment(NotificationManager.self) private var notificationManager - @Environment(UserMetaDataManager.self) private var userMetaDataManager + @State private var viewState = ViewState.idle var body: some View { - @Bindable var userMetaDataManager = userMetaDataManager - let notificationSettings = $userMetaDataManager.notificationSettings - List { - Group { - Section { - Toggle("Appointments", isOn: notificationSettings.receivesAppointmentReminders) - Toggle("Survey", isOn: notificationSettings.receivesQuestionnaireReminders) - Toggle("Vitals", isOn: notificationSettings.receivesVitalsReminders) - } header: { - Text("Reminders") - } footer: { - Text("Receive reminders for appointments (one day before), symptom surveys, and vital measurements.") - } - Section { - Toggle("Medications", isOn: notificationSettings.receivesMedicationUpdates) - Toggle("Recommendations", isOn: notificationSettings.receivesRecommendationUpdates) - } header: { - Text("Updates") - } footer: { - Text("Receive updates when current medications and medication recommendations change.") - } - Section { - Toggle("Weight Trends", isOn: notificationSettings.receivesWeightAlerts) - } header: { - Text("Trends") - } footer: { - Text("Receive notifications of changes in vital trends.") - } - } - .disabled(!self.notificationManager.notificationsAuthorized) if !self.notificationManager.notificationsAuthorized { AsyncButton("Enable Notifications in Settings") { // Create the URL that deep links to notification settings. - if let url = URL(string: UIApplication.openNotificationSettingsURLString) { + if let url = URL(string: UIApplication.openNotificationSettingsURLString), + UIApplication.shared.canOpenURL(url) { // Ask the system to open that URL. await UIApplication.shared.open(url) } } } + + Group { + remindersSection + updatesSection + trendsSection + } + .disabled(!self.notificationManager.notificationsAuthorized) } .task { await notificationManager.checkNotificationsAuthorized() } - .onChange(of: userMetaDataManager.notificationSettings) { - Task { - await userMetaDataManager.pushUpdatedNotificationSettings() + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + ProgressView() + .opacity(viewState == .processing ? 1 : 0) + .animation(.default, value: viewState) } } .navigationTitle("Notifications") + .viewStateAlert(state: $viewState) + } + + private var remindersSection: some View { + Section { + toggleRow("Appointments", for: AccountKeys.receivesAppointmentReminders) + toggleRow("Inactivity", for: AccountKeys.receivesInactivityReminders) + toggleRow("Survey", for: AccountKeys.receivesQuestionnaireReminders) + toggleRow("Vitals", for: AccountKeys.receivesVitalsReminders) + } header: { + Text("Reminders") + } footer: { + Text("Receive reminders for appointments (one day before), symptom surveys, and vital measurements.") + } + } + + private var updatesSection: some View { + Section { + toggleRow("Medications", for: AccountKeys.receivesMedicationUpdates) + toggleRow("Recommendations", for: AccountKeys.receivesRecommendationUpdates) + } header: { + Text("Updates") + } footer: { + Text("Receive updates when current medications and medication recommendations change.") + } + } + + private var trendsSection: some View { + Section { + toggleRow("Weight Trends", for: AccountKeys.receivesWeightAlerts) + } header: { + Text("Trends") + } footer: { + Text("Receive notifications of changes in vital trends.") + } + } + + private func toggleRow(_ title: String, for key: Key.Type) -> some View where Key.Value == Bool { + Toggle(title, isOn: account.detailsBinding(for: key, viewState: $viewState)) } } diff --git a/ENGAGEHF/ContentView.swift b/ENGAGEHF/ContentView.swift index 45aa89b8..f231baca 100644 --- a/ENGAGEHF/ContentView.swift +++ b/ENGAGEHF/ContentView.swift @@ -1,55 +1,103 @@ // -// ContentView.swift -// ENGAGEHF +// This source file is part of the ENGAGE-HF project based on the Stanford Spezi Template Application project // -// Created by Paul Kraft on 04.09.2024. +// SPDX-FileCopyrightText: 2023 Stanford University +// +// SPDX-License-Identifier: MIT // @_spi(TestingSupport) import SpeziAccount +import SpeziOnboarding import SpeziViews import SwiftUI @MainActor struct ContentView: View { - @AppStorage(StorageKeys.onboardingFlowComplete) var completedOnboardingFlow = false + private enum SheetContent: String, Identifiable { + case onboarding + case auth + + var id: String { + rawValue + } + } + + @AppStorage(StorageKeys.onboardingFlowComplete) private var completedOnboardingFlow = false @Environment(Account.self) private var account: Account - @State private var isLoginActive = false - @State private var isPresentingLogin = false + @State private var sheetContent: SheetContent? - private var shouldPresentLogin: Bool { - guard !FeatureFlags.disableFirebase - && !FeatureFlags.skipOnboarding - && completedOnboardingFlow else { - return false - } - return !account.signedIn - || (account.details?.isAnonymous ?? true) - || isLoginActive + private var expectedSheetContent: SheetContent? { + guard FeatureFlags.skipOnboarding || completedOnboardingFlow else { + return .onboarding + } + guard FeatureFlags.disableFirebase || account.signedIn else { + return .auth + } + guard FeatureFlags.disableFirebase + || account.details?.isIncomplete ?? true + || account.details?.invitationCode != nil else { + return .auth + } + return nil } - + var body: some View { ZStack { - if !completedOnboardingFlow { - EmptyView() - } else if isPresentingLogin { - EmptyView() + 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() + } + } } else { HomeView() } } - .onChange(of: shouldPresentLogin, initial: true) { - Task { @MainActor in - // This task makes sure to show the sheet slightly after the view, - // so that the presentation will actually occur - otherwise it might - // not happen - isPresentingLogin = shouldPresentLogin - } + .onChange(of: expectedSheetContent, initial: true) { + Task { @MainActor in + // Delaying this update by 0.5 seconds 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: !$completedOnboardingFlow) { - OnboardingFlow() + } + .sheet(isPresented: $sheetContent.exists()) { + Group { + switch sheetContent { + case .onboarding: + OnboardingFlow() + case .auth: + AuthFlow() + case .none: + EmptyView() + } } - .sheet(isPresented: $isPresentingLogin) { - AccountSetupSheet(isLoginActive: $isLoginActive) + .interactiveDismissDisabled(true) + } + } + + @MainActor + private func updateSheetContent() { + sheetContent = expectedSheetContent + } +} + +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 18bf158e..5f0660f5 100644 --- a/ENGAGEHF/ENGAGEHFDelegate.swift +++ b/ENGAGEHF/ENGAGEHFDelegate.swift @@ -24,19 +24,27 @@ import SwiftUI class ENGAGEHFDelegate: SpeziAppDelegate { override var configuration: Configuration { + // swiftlint:disable:next closure_body_length Configuration(standard: ENGAGEHFStandard()) { if !FeatureFlags.disableFirebase { AccountConfiguration( service: FirebaseAccountService(providers: [.emailAndPassword, .signInWithApple], emulatorSettings: accountEmulator), storageProvider: FirestoreAccountStorage(storeIn: Firestore.userCollection, mapping: [ - // ENGAGE was originally deployed with SpeziAccount 1.0 and key identifiers change with SpeziAccount 2.0. - // Therefore, we need to provide a backwards compatibility mapping. - "DateOfBirthKey": AccountKeys.dateOfBirth + "dateOfBirth": AccountKeys.dateOfBirth, + "invitationCode": AccountKeys.invitationCode ]), configuration: [ .requires(\.userId), .supports(\.name), - .supports(\.dateOfBirth) + .hidden(\.invitationCode), + .hidden(\.organization), + .hidden(\.receivesAppointmentReminders), + .hidden(\.receivesInactivityReminders), + .hidden(\.receivesMedicationUpdates), + .hidden(\.receivesQuestionnaireReminders), + .hidden(\.receivesRecommendationUpdates), + .hidden(\.receivesVitalsReminders), + .hidden(\.receivesWeightAlerts) ] ) diff --git a/ENGAGEHF/Managers/MedicationsManager/MedicationsManager.swift b/ENGAGEHF/Managers/MedicationsManager/MedicationsManager.swift index c40bada8..eeacc1c3 100644 --- a/ENGAGEHF/Managers/MedicationsManager/MedicationsManager.swift +++ b/ENGAGEHF/Managers/MedicationsManager/MedicationsManager.swift @@ -50,13 +50,10 @@ class MedicationsManager: Module, EnvironmentAccessible { return } - switch event { - case let .associatedAccount(details): + if let details = event.newEnrolledAccountDetails { updateSnapshotListener(for: details) - case .disassociatingAccount: + } else if event.accountDetails == nil { updateSnapshotListener(for: nil) - default: - break } } } diff --git a/ENGAGEHF/Managers/MessageManager/MessageManager.swift b/ENGAGEHF/Managers/MessageManager/MessageManager.swift index ed92c63d..15e4f0ca 100644 --- a/ENGAGEHF/Managers/MessageManager/MessageManager.swift +++ b/ENGAGEHF/Managers/MessageManager/MessageManager.swift @@ -54,13 +54,10 @@ final class MessageManager: Module, EnvironmentAccessible, DefaultInitializable return } - switch event { - case let .associatedAccount(details): + if let details = event.newEnrolledAccountDetails { updateSnapshotListener(for: details) - case .disassociatingAccount: + } else if event.accountDetails == nil { updateSnapshotListener(for: nil) - default: - break } } } diff --git a/ENGAGEHF/Managers/NavigationManager/NavigationManager.swift b/ENGAGEHF/Managers/NavigationManager/NavigationManager.swift index a0c41ede..2cd9e373 100644 --- a/ENGAGEHF/Managers/NavigationManager/NavigationManager.swift +++ b/ENGAGEHF/Managers/NavigationManager/NavigationManager.swift @@ -37,25 +37,26 @@ class NavigationManager: Module, EnvironmentAccessible { // On sign in, reinitialize to an empty navigation path func configure() { - if let accountNotifications { - notificationTask = Task.detached { @MainActor [weak self] in - for await event in accountNotifications.events { - guard let self else { - return - } - guard case .associatedAccount = event else { - continue - } + guard let accountNotifications else { + preconditionFailure("Expected account notifications to be availble.") + } + notificationTask = Task.detached { @MainActor [weak self] in + for await event in accountNotifications.events { + guard let self else { + return + } + guard event.newEnrolledAccountDetails != nil else { + continue + } - logger.debug("Reinitializing navigation path.") + logger.debug("Reinitializing navigation path.") - educationPath = NavigationPath() - medicationsPath = NavigationPath() - heartHealthPath = NavigationPath() - homePath = NavigationPath() + educationPath = NavigationPath() + medicationsPath = NavigationPath() + heartHealthPath = NavigationPath() + homePath = NavigationPath() - selectedTab = .home - } + selectedTab = .home } } } diff --git a/ENGAGEHF/Managers/NotificationManager/NotificationManager.swift b/ENGAGEHF/Managers/NotificationManager/NotificationManager.swift index 471fb664..b43f80b3 100644 --- a/ENGAGEHF/Managers/NotificationManager/NotificationManager.swift +++ b/ENGAGEHF/Managers/NotificationManager/NotificationManager.swift @@ -64,8 +64,7 @@ class NotificationManager: Module, NotificationHandler, NotificationTokenHandler return } - switch event { - case .associatedAccount: + if event.newEnrolledAccountDetails != nil { do { _ = try await self.requestNotificationPermissions() } catch { @@ -76,9 +75,9 @@ class NotificationManager: Module, NotificationHandler, NotificationTokenHandler ) ) } - case .disassociatingAccount: + } else if event.accountDetails == nil { do { - _ = try await self.unregisterDeviceToken() + _ = try? await self.unregisterDeviceToken() } catch { self.state = .error( AnyLocalizedError( @@ -87,8 +86,6 @@ class NotificationManager: Module, NotificationHandler, NotificationTokenHandler ) ) } - default: - break } } } @@ -145,7 +142,7 @@ class NotificationManager: Module, NotificationHandler, NotificationTokenHandler /// "action": "medications" /// } let payload = response.notification.request.content.userInfo["action"] as? String - await _ = navigationManager.execute(MessageAction(from: payload)) + _ = await navigationManager.execute(MessageAction(from: payload)) } @@ -170,13 +167,7 @@ class NotificationManager: Module, NotificationHandler, NotificationTokenHandler do { try await self.configureRemoteNotifications(using: deviceToken) } catch { - self.logger.error("Failed to configured remote notifications for updated device token: \(error)") - self.state = .error( - AnyLocalizedError( - error: error, - defaultErrorDescription: "Unable to unregister for remote notifications." - ) - ) + self.logger.error("Failed to configure remote notifications for updated device token: \(error)") } } } diff --git a/ENGAGEHF/Managers/UserMetaDataManager/NotificationSettings.swift b/ENGAGEHF/Managers/UserMetaDataManager/NotificationSettings.swift deleted file mode 100644 index 33288802..00000000 --- a/ENGAGEHF/Managers/UserMetaDataManager/NotificationSettings.swift +++ /dev/null @@ -1,65 +0,0 @@ -// -// 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 Foundation -import SwiftUI - - -/// The user's preferences for whether or not to receive each type of Push Notification -struct NotificationSettings: Codable, Equatable { - var receivesAppointmentReminders: Bool - var receivesMedicationUpdates: Bool - var receivesQuestionnaireReminders: Bool - var receivesRecommendationUpdates: Bool - var receivesVitalsReminders: Bool - var receivesWeightAlerts: Bool - - - var codingRepresentation: [String: Bool] { - [ - CodingKeys.receivesAppointmentReminders.stringValue: receivesAppointmentReminders, - CodingKeys.receivesMedicationUpdates.stringValue: receivesMedicationUpdates, - CodingKeys.receivesQuestionnaireReminders.stringValue: receivesQuestionnaireReminders, - CodingKeys.receivesRecommendationUpdates.stringValue: receivesRecommendationUpdates, - CodingKeys.receivesVitalsReminders.stringValue: receivesVitalsReminders, - CodingKeys.receivesWeightAlerts.stringValue: receivesWeightAlerts - ] - } - - - init( - receivesAppointmentReminders: Bool = true, - receivesMedicationUpdates: Bool = true, - receivesQuestionnaireReminders: Bool = true, - receivesRecommendationUpdates: Bool = true, - receivesVitalsReminders: Bool = true, - receivesWeightAlerts: Bool = true - ) { - self.receivesAppointmentReminders = receivesAppointmentReminders - self.receivesMedicationUpdates = receivesMedicationUpdates - self.receivesQuestionnaireReminders = receivesQuestionnaireReminders - self.receivesRecommendationUpdates = receivesRecommendationUpdates - self.receivesVitalsReminders = receivesVitalsReminders - self.receivesWeightAlerts = receivesWeightAlerts - } -} - - -extension NotificationSettings { - init(from decoder: any Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - - // NOTE: If no setting is found, defaults to false, but the user can still toggle the setting on from the client. - self.receivesAppointmentReminders = try container.decodeIfPresent(Bool.self, forKey: .receivesAppointmentReminders) ?? false - self.receivesMedicationUpdates = try container.decodeIfPresent(Bool.self, forKey: .receivesMedicationUpdates) ?? false - self.receivesQuestionnaireReminders = try container.decodeIfPresent(Bool.self, forKey: .receivesQuestionnaireReminders) ?? false - self.receivesRecommendationUpdates = try container.decodeIfPresent(Bool.self, forKey: .receivesRecommendationUpdates) ?? false - self.receivesVitalsReminders = try container.decodeIfPresent(Bool.self, forKey: .receivesVitalsReminders) ?? false - self.receivesWeightAlerts = try container.decodeIfPresent(Bool.self, forKey: .receivesWeightAlerts) ?? false - } -} diff --git a/ENGAGEHF/Managers/UserMetaDataManager/OrganizationInformation.swift b/ENGAGEHF/Managers/UserMetaDataManager/Organization.swift similarity index 82% rename from ENGAGEHF/Managers/UserMetaDataManager/OrganizationInformation.swift rename to ENGAGEHF/Managers/UserMetaDataManager/Organization.swift index bbc74165..96c63255 100644 --- a/ENGAGEHF/Managers/UserMetaDataManager/OrganizationInformation.swift +++ b/ENGAGEHF/Managers/UserMetaDataManager/Organization.swift @@ -10,12 +10,7 @@ import Foundation import SpeziContact -struct OrganizationIdentifier: Decodable { - let organization: String? -} - - -struct OrganizationInformation: Decodable { +struct Organization: Decodable { let name: String let contactName: String let phoneNumber: String @@ -43,8 +38,8 @@ struct OrganizationInformation: Decodable { #if TEST || DEBUG -extension OrganizationInformation { - static let testOrganization = OrganizationInformation( +extension Organization { + static let test = Organization( name: "Stanford University", contactName: "Leland Stanford Jr.", phoneNumber: "(111) 111-1111", diff --git a/ENGAGEHF/Managers/UserMetaDataManager/UserMetaDataManager.swift b/ENGAGEHF/Managers/UserMetaDataManager/UserMetaDataManager.swift index cd53d59d..362d5e56 100644 --- a/ENGAGEHF/Managers/UserMetaDataManager/UserMetaDataManager.swift +++ b/ENGAGEHF/Managers/UserMetaDataManager/UserMetaDataManager.swift @@ -18,15 +18,13 @@ import SpeziFirebaseAccount class UserMetaDataManager: Module, EnvironmentAccessible { @ObservationIgnored @Dependency(Account.self) private var account: Account? @ObservationIgnored @Dependency(AccountNotifications.self) private var accountNotifications: AccountNotifications? - @ObservationIgnored @Dependency(FirebaseAccountService.self) private var accountService: FirebaseAccountService? @ObservationIgnored @Application(\.logger) private var logger private var snapshotListener: ListenerRegistration? private var notificationsTask: Task? - private var previousOrganizationId: String = "" + private var previousOrganizationId: String? - private(set) var organization: OrganizationInformation? - var notificationSettings = NotificationSettings() + private(set) var organization: Organization? func configure() { @@ -34,131 +32,43 @@ class UserMetaDataManager: Module, EnvironmentAccessible { return } - // Clear away any previous user's organization so that we do not skip fetching the organization info - // for the newly-signed in user. - self.organization = nil - - // On sign in, store the user's organization and message settings if let accountNotifications { notificationsTask = Task.detached { @MainActor [weak self] in for await event in accountNotifications.events { guard let self else { return } - - switch event { - case let .associatedAccount(details): - updateSnapshotListener(for: details) - case .disassociatingAccount: - updateSnapshotListener(for: nil) - default: - break - } + + updateOrganizationIfNeeded(id: event.accountDetails?.organization) } } } if let account { - updateSnapshotListener(for: account.details) + updateOrganizationIfNeeded(id: account.details?.organization) } } - - /// Call on change of notification settings in Account Sheet. - /// Updates the user's notification settings in Firestore to the current values stored in the manager. - func pushUpdatedNotificationSettings() async { - self.logger.debug("Updating notification settings.") - - guard let details = account?.details else { - return - } - - let userDocRef = Firestore.userDocumentReference(for: details.accountId) - - do { - try await userDocRef.updateData(self.notificationSettings.codingRepresentation) - } catch { - self.logger.error("Failed to update notification settings: \(error)") - return - } - } - - - /// Called on sign-in. Registers a snapshot listener to the user's meta-data document in Firestore. - /// Collects information such as notification preferences and organization contact information. - private func updateSnapshotListener(for details: AccountDetails?) { - self.logger.debug("Initializing user information snapshot listener...") - - self.snapshotListener?.remove() - - guard let details else { - return - } - - let userDocRef = Firestore.userDocumentReference(for: details.accountId) - - self.snapshotListener = userDocRef - .addSnapshotListener { docSnapshot, error in - if let error { - self.logger.error("Failed to initialize user information snapshot: \(error)") - return - } - - self.logger.debug("Fetching user metadata.") - - guard let userDoc = docSnapshot else { - self.logger.error("No document found in user document snapshot.") - return - } - - // Fetch the organization info if the organization identifier has changed - self.getOrganizationInfo(from: userDoc) - - // Decode message settings - // Defaults to true if field not present in firestore, and ignores unknown fields - do { - self.notificationSettings = try userDoc.data(as: NotificationSettings.self) - self.logger.debug("Successfully fetched message settings.") - } catch { - self.logger.error("Failed to decode message settings: \(error)") - } - } - } - - private func getOrganizationInfo(from userDoc: DocumentSnapshot) { - self.logger.debug("Fetching organization from \(userDoc.documentID).") - -#if TEST || DEBUG - if FeatureFlags.setupTestUserMetaData { - self.organization = .testOrganization + private func updateOrganizationIfNeeded(id organizationId: String?) { + guard previousOrganizationId != organizationId else { return } -#endif - - let organizationIdWrapper: OrganizationIdentifier - do { - organizationIdWrapper = try userDoc.data(as: OrganizationIdentifier.self) - } catch { - self.logger.error("Failed to decode organization identifier: \(error)") + previousOrganizationId = organizationId + organization = nil + guard let organizationId else { return } - - guard let organizationId = organizationIdWrapper.organization else { - self.logger.error("No organization id found.") - return - } - - guard organizationId != self.previousOrganizationId else { - return - } - let organizationDocRef = Firestore.organizationCollectionReference.document(organizationId) - Task { @MainActor in do { - self.organization = try await organizationDocRef.getDocument(as: OrganizationInformation.self) - self.previousOrganizationId = organizationId - +#if TEST + if FeatureFlags.setupTestUserMetaData { + self.organization = .testOrganization + self.logger.debug("Injected test organization.") + return + } +#endif + self.organization = try await organizationDocRef.getDocument(as: Organization.self) self.logger.debug("Successfully fetched organization information.") } catch { self.logger.error("Failed to fetch contact information for organization \(organizationId): \(error)") diff --git a/ENGAGEHF/Managers/VideoManager/VideoManager.swift b/ENGAGEHF/Managers/VideoManager/VideoManager.swift index 1f4062b4..2807243b 100644 --- a/ENGAGEHF/Managers/VideoManager/VideoManager.swift +++ b/ENGAGEHF/Managers/VideoManager/VideoManager.swift @@ -12,7 +12,6 @@ import OSLog import Spezi import SpeziAccount - @Observable @MainActor final class VideoManager: Module, EnvironmentAccessible, DefaultInitializable { @@ -43,7 +42,7 @@ final class VideoManager: Module, EnvironmentAccessible, DefaultInitializable { return } - if case .associatedAccount = event { + if event.newEnrolledAccountDetails != nil { videoCollections = await getVideoSections() } } diff --git a/ENGAGEHF/Managers/VitalsManager/VitalsManager.swift b/ENGAGEHF/Managers/VitalsManager/VitalsManager.swift index 72c40659..2a6a522d 100644 --- a/ENGAGEHF/Managers/VitalsManager/VitalsManager.swift +++ b/ENGAGEHF/Managers/VitalsManager/VitalsManager.swift @@ -70,8 +70,7 @@ public class VitalsManager: Module, EnvironmentAccessible { return } - switch event { - case let .associatedAccount(details): + if let details = event.newEnrolledAccountDetails { updateSnapshotListener(for: details) /// If testing, add mock measurements to the user's heart rate, blood pressure, weight, and symptoms histories @@ -83,18 +82,12 @@ public class VitalsManager: Module, EnvironmentAccessible { logger.error("Failed to setup Heart Health testing: \(error)") } } - case .disassociatingAccount: + } else if event.accountDetails == nil { updateSnapshotListener(for: nil) - default: - break } } } } - - if let account { - updateSnapshotListener(for: account.details) - } } private func updateSnapshotListener(for details: AccountDetails?) { diff --git a/ENGAGEHF/Onboarding/AccountFinish.swift b/ENGAGEHF/Onboarding/AccountFinish.swift deleted file mode 100644 index 931d043f..00000000 --- a/ENGAGEHF/Onboarding/AccountFinish.swift +++ /dev/null @@ -1,79 +0,0 @@ -// -// 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 -// - -@_spi(TestingSupport) import SpeziAccount -import SpeziOnboarding -import SpeziViews -import SwiftUI - - -struct AccountFinish: View { - @Environment(Account.self) private var account - @Environment(OnboardingNavigationPath.self) private var onboardingNavigationPath - - @State private var viewState = ViewState.processing - - var body: some View { - VStack { - switch viewState { - case .idle: - Text("Account Creation Finished!") - .font(.title) - Text("Your account has successfully been created. You may now proceed.") - .font(.headline) - - Button("Continue") { - onboardingNavigationPath.nextStep() - } - case .processing: - ProgressView() - case let .error(error): - Text("Error occured: \(error.errorDescription ?? "")") - - Button("Try again") { - onboardingNavigationPath.removeLast() - } - } - } - .task { - do { - try await account.finishSetupIfNeeded() - viewState = .idle - } catch { - viewState = .error(AnyLocalizedError(error: error)) - } - } - .navigationTitle(Text("Finishing Account Setup")) - .navigationBarBackButtonHidden(true) - } -} - - -#if DEBUG -#Preview("Account Finish SignIn") { - OnboardingStack { - AccountFinish() - } - .previewWith { - AccountConfiguration(service: InMemoryAccountService()) - } -} - -#Preview("Account Finish SignUp") { - var details = AccountDetails() - details.userId = "lelandstanford@stanford.edu" - details.name = PersonNameComponents(givenName: "Leland", familyName: "Stanford") - - return OnboardingStack { - AccountFinish() - } - .previewWith { - AccountConfiguration(service: InMemoryAccountService(), activeDetails: details) - } -} -#endif diff --git a/ENGAGEHF/Onboarding/AccountOnboarding.swift b/ENGAGEHF/Onboarding/AccountOnboarding.swift index edc222b6..52b6471e 100644 --- a/ENGAGEHF/Onboarding/AccountOnboarding.swift +++ b/ENGAGEHF/Onboarding/AccountOnboarding.swift @@ -16,25 +16,15 @@ import SwiftUI struct AccountOnboarding: View { @Environment(Account.self) private var account @Environment(OnboardingNavigationPath.self) private var onboardingNavigationPath - - @MainActor private var setupStyle: PreferredSetupStyle { - if let details = account.details, - details.isAnonymous { - .signup - } else { - // when we navigate here from the InvitationCodeView we remove the anonymous account for sign in. - .login - } - } @State private var viewState = ViewState.idle var body: some View { - AccountSetup { _ in - 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. + AccountSetup { details in + if details.invitationCode != nil { onboardingNavigationPath.nextStep() + } else { + onboardingNavigationPath.append(customView: InvitationCodeView()) } } header: { AccountSetupHeader() @@ -42,11 +32,15 @@ struct AccountOnboarding: View { OnboardingActionsView( "ACCOUNT_NEXT", action: { - onboardingNavigationPath.nextStep() + if account.details?.invitationCode != nil { + onboardingNavigationPath.nextStep() + } else { + onboardingNavigationPath.append(customView: InvitationCodeView()) + } } ) } - .preferredAccountSetupStyle(setupStyle) + .preferredAccountSetupStyle(.login) .viewStateAlert(state: $viewState) } } diff --git a/ENGAGEHF/Onboarding/AuthFlow.swift b/ENGAGEHF/Onboarding/AuthFlow.swift new file mode 100644 index 00000000..f579b519 --- /dev/null +++ b/ENGAGEHF/Onboarding/AuthFlow.swift @@ -0,0 +1,19 @@ +// +// 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 +// + +@_spi(TestingSupport) import SpeziAccount +import SpeziOnboarding +import SwiftUI + +struct AuthFlow: View { + var body: some View { + OnboardingStack { + AccountOnboarding() + } + } +} diff --git a/ENGAGEHF/Onboarding/InvitationCodeView.swift b/ENGAGEHF/Onboarding/InvitationCodeView.swift index 71f22718..608ab8f1 100644 --- a/ENGAGEHF/Onboarding/InvitationCodeView.swift +++ b/ENGAGEHF/Onboarding/InvitationCodeView.swift @@ -6,19 +6,21 @@ // SPDX-License-Identifier: MIT // +import SpeziAccount import SpeziOnboarding import SpeziValidation import SpeziViews import SwiftUI - +@MainActor struct InvitationCodeView: View { @Environment(OnboardingNavigationPath.self) private var onboardingNavigationPath @Environment(InvitationCodeModule.self) private var invitationCodeModule + @Environment(Account.self) private var account @State private var invitationCode = "" @State private var viewState: ViewState = .idle - + @ValidationState private var validation @@ -42,26 +44,7 @@ struct InvitationCodeView: View { .padding(.top, -8) .padding(.bottom, -12) Divider() - OnboardingActionsView( - primaryText: "Redeem Invitation Code", - primaryAction: { - guard validation.validateSubviews() else { - return - } - - do { - try await invitationCodeModule.verifyOnboardingCode(invitationCode) - onboardingNavigationPath.nextStep() - } catch { - viewState = .error(AnyLocalizedError(error: error)) - } - }, - secondaryText: "I Already Have an Account", - secondaryAction: { - await invitationCodeModule.clearAccount() - onboardingNavigationPath.nextStep() - } - ) + actionsView } .padding(.horizontal) .padding(.bottom) @@ -69,8 +52,34 @@ struct InvitationCodeView: View { .navigationBarTitleDisplayMode(.large) .navigationTitle(String(localized: "Invitation Code")) } + .navigationBarBackButtonHidden() } + @ViewBuilder private var actionsView: some View { + OnboardingActionsView( + primaryText: "Redeem Invitation Code", + primaryAction: { + guard validation.validateSubviews() else { + return + } + do { + try await invitationCodeModule.verifyOnboardingCode(invitationCode) + onboardingNavigationPath.nextStep() + } catch { + viewState = .error(AnyLocalizedError(error: error)) + } + }, + secondaryText: "Logout", + secondaryAction: { + do { + try await account.accountService.logout() + onboardingNavigationPath.removeLast() + } catch { + viewState = .error(AnyLocalizedError(error: error)) + } + } + ) + } @ViewBuilder private var invitationCodeView: some View { DescriptionGridRow { @@ -99,9 +108,6 @@ struct InvitationCodeView: View { Text("Please enter your invitation code to join the ENGAGE-HF study.") } } - - - init() {} } diff --git a/ENGAGEHF/Onboarding/OnboardingFlow.swift b/ENGAGEHF/Onboarding/OnboardingFlow.swift index 3235ecd0..97ff536d 100644 --- a/ENGAGEHF/Onboarding/OnboardingFlow.swift +++ b/ENGAGEHF/Onboarding/OnboardingFlow.swift @@ -11,7 +11,6 @@ import SpeziFirebaseAccount import SpeziOnboarding import SwiftUI - /// Displays an multi-step onboarding flow for the ENGAGEHF. struct OnboardingFlow: View { @AppStorage(StorageKeys.onboardingFlowComplete) private var completedOnboardingFlow = false @@ -21,16 +20,11 @@ struct OnboardingFlow: View { OnboardingStack(onboardingFlowComplete: $completedOnboardingFlow) { Welcome() InterestingModules() - if !FeatureFlags.disableFirebase { - InvitationCodeView() AccountOnboarding() - AccountFinish() } - NotificationPermissions() } - .interactiveDismissDisabled(!completedOnboardingFlow) } } diff --git a/ENGAGEHF/Resources/Localizable.xcstrings b/ENGAGEHF/Resources/Localizable.xcstrings index 1182f488..3bcfdb0f 100644 --- a/ENGAGEHF/Resources/Localizable.xcstrings +++ b/ENGAGEHF/Resources/Localizable.xcstrings @@ -45,9 +45,6 @@ }, "About %@" : { - }, - "Account Creation Finished!" : { - }, "ACCOUNT_NEXT" : { "localizations" : { @@ -189,7 +186,7 @@ "Content ..." : { }, - "Continue" : { + "Content Unavailable" : { }, "Current" : { @@ -215,9 +212,6 @@ }, "Enable Notifications in Settings" : { - }, - "Error occured: %@" : { - }, "Expansion Button" : { @@ -233,9 +227,6 @@ }, "Failed to save new %@ measurement." : { - }, - "Finishing Account Setup" : { - }, "Generating Health Summary" : { @@ -267,7 +258,7 @@ "Home" : { }, - "I Already Have an Account" : { + "Inactivity" : { }, "Input: %@" : { @@ -422,6 +413,9 @@ }, "List Without Sections" : { + }, + "Logout" : { + }, "Medication Label: %@" : { @@ -493,6 +487,9 @@ }, "Notifications" : { + }, + "Organization" : { + }, "Play Video" : { @@ -529,6 +526,9 @@ }, "Resolution Picker" : { + }, + "Retry" : { + }, "Review" : { @@ -667,6 +667,9 @@ }, "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." : { + }, "Thumbnail Image: %@" : { @@ -685,9 +688,6 @@ }, "Trigger Weight Measurement" : { - }, - "Try again" : { - }, "Unable to create time interval for date: %@" : { @@ -861,9 +861,6 @@ } } } - }, - "Your account has successfully been created. You may now proceed." : { - } }, "version" : "1.0" diff --git a/ENGAGEHF/SharedExtensions/Account+Binding.swift b/ENGAGEHF/SharedExtensions/Account+Binding.swift new file mode 100644 index 00000000..56d53637 --- /dev/null +++ b/ENGAGEHF/SharedExtensions/Account+Binding.swift @@ -0,0 +1,52 @@ +// +// 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 SpeziAccount +import SpeziViews +import SwiftUI + +extension Account { + @MainActor + func detailsBinding( + for key: Key.Type, + viewState: Binding + ) -> Binding { + Binding { + self.details?[key] ?? key.initialValue.value + } set: { newValue in + var modifiedDetails = AccountDetails() + modifiedDetails[key] = newValue + guard let modifications = try? AccountModifications(modifiedDetails: modifiedDetails) else { + return + } + Task { @MainActor in + guard viewState.wrappedValue == .idle else { + return + } + viewState.wrappedValue = .processing + do { + try await self.accountService.updateAccountDetails(modifications) + viewState.wrappedValue = .idle + } catch { + viewState.wrappedValue = .error(AnyLocalizedError(error: error)) + } + } + } + } +} + +extension InitialValue { + fileprivate var value: Value { + switch self { + case let .empty(value): + return value + case let .default(value): + return value + } + } +} diff --git a/ENGAGEHF/SharedExtensions/AccountDetails+Key.swift b/ENGAGEHF/SharedExtensions/AccountDetails+Key.swift new file mode 100644 index 00000000..4cb2ec63 --- /dev/null +++ b/ENGAGEHF/SharedExtensions/AccountDetails+Key.swift @@ -0,0 +1,106 @@ +// +// 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 Foundation +import SpeziAccount + +// swiftlint:disable attributes + +extension AccountDetails { + @AccountKey( + id: "invitationCode", + name: "Invitation Code", + category: .other, + as: String.self, + initial: .empty("") + ) + var invitationCode: String? + + @AccountKey( + id: "organization", + name: "Organization", + category: .other, + as: String.self, + initial: .empty("") + ) + var organization: String? + + @AccountKey( + id: "receivesAppointmentReminders", + name: "Appointments", + category: .other, + as: Bool.self, + initial: .default(true) + ) + var receivesAppointmentReminders: Bool + + @AccountKey( + id: "receivesInactivityReminders", + name: "Inactivity", + category: .other, + as: Bool.self, + initial: .default(true) + ) + var receivesInactivityReminders: Bool + + @AccountKey( + id: "receivesMedicationUpdates", + name: "Medications", + category: .other, + as: Bool.self, + initial: .default(true) + ) + var receivesMedicationUpdates: Bool + + @AccountKey( + id: "receivesQuestionnaireReminders", + name: "Survey", + category: .other, + as: Bool.self, + initial: .default(true) + ) + var receivesQuestionnaireReminders: Bool + + @AccountKey( + id: "receivesRecommendationUpdates", + name: "Recommendations", + category: .other, + as: Bool.self, + initial: .default(true) + ) + var receivesRecommendationUpdates: Bool + + @AccountKey( + id: "receivesVitalsReminders", + name: "Vitals", + category: .other, + as: Bool.self, + initial: .default(true) + ) + var receivesVitalsReminders: Bool + + @AccountKey( + id: "receivesWeightAlerts", + name: "Weight Trends", + category: .other, + as: Bool.self, + initial: .default(true) + ) + var receivesWeightAlerts: Bool +} + +@KeyEntry(\.invitationCode) +@KeyEntry(\.organization) +@KeyEntry(\.receivesAppointmentReminders) +@KeyEntry(\.receivesInactivityReminders) +@KeyEntry(\.receivesMedicationUpdates) +@KeyEntry(\.receivesQuestionnaireReminders) +@KeyEntry(\.receivesRecommendationUpdates) +@KeyEntry(\.receivesVitalsReminders) +@KeyEntry(\.receivesWeightAlerts) +extension AccountKeys {} diff --git a/ENGAGEHF/SharedExtensions/AccountNotifications+Extras.swift b/ENGAGEHF/SharedExtensions/AccountNotifications+Extras.swift new file mode 100644 index 00000000..31bb3e98 --- /dev/null +++ b/ENGAGEHF/SharedExtensions/AccountNotifications+Extras.swift @@ -0,0 +1,33 @@ +// +// 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 SpeziAccount + +extension AccountNotifications.Event { + var newEnrolledAccountDetails: AccountDetails? { + switch self { + case let .associatedAccount(details) where details.invitationCode != nil: + return details + case let .detailsChanged(before, after) where before.invitationCode == nil && after.invitationCode != nil: + return after + case .associatedAccount, .detailsChanged, .disassociatingAccount, .deletingAccount: + return nil + } + } + + var accountDetails: AccountDetails? { + switch self { + case let .associatedAccount(details): + return details + case let .detailsChanged(_, after): + return after + case .deletingAccount, .disassociatingAccount: + return nil + } + } +} From 392c63c4946629d056eee0bf82ff1cf37bcab63e Mon Sep 17 00:00:00 2001 From: Paul Kraft Date: Thu, 12 Sep 2024 11:37:17 -0700 Subject: [PATCH 5/5] Specify one branch --- ENGAGEHF.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/swiftpm/Package.resolved | 4 ++-- .../NotificationManager/NotificationManager.swift | 13 ++----------- 3 files changed, 5 insertions(+), 14 deletions(-) diff --git a/ENGAGEHF.xcodeproj/project.pbxproj b/ENGAGEHF.xcodeproj/project.pbxproj index 52cb1632..89a009b0 100644 --- a/ENGAGEHF.xcodeproj/project.pbxproj +++ b/ENGAGEHF.xcodeproj/project.pbxproj @@ -1992,7 +1992,7 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/StanfordSpezi/SpeziAccount.git"; requirement = { - branch = "feature/async-throwing-setup"; + branch = "add-hidden-requirement"; kind = branch; }; }; diff --git a/ENGAGEHF.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ENGAGEHF.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 6d858150..64fcb680 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" : "feature/async-throwing-setup", - "revision" : "63cf7b601020942bdce4c601f9b01d835863b65a" + "branch" : "add-hidden-requirement", + "revision" : "a3fe2bf688b06a8ef7e117e7e091c250b10e28f4" } }, { diff --git a/ENGAGEHF/Managers/NotificationManager/NotificationManager.swift b/ENGAGEHF/Managers/NotificationManager/NotificationManager.swift index b43f80b3..906e2e67 100644 --- a/ENGAGEHF/Managers/NotificationManager/NotificationManager.swift +++ b/ENGAGEHF/Managers/NotificationManager/NotificationManager.swift @@ -76,16 +76,7 @@ class NotificationManager: Module, NotificationHandler, NotificationTokenHandler ) } } else if event.accountDetails == nil { - do { - _ = try? await self.unregisterDeviceToken() - } catch { - self.state = .error( - AnyLocalizedError( - error: error, - defaultErrorDescription: "Unable to unregister for remote notifications." - ) - ) - } + _ = try? await self.unregisterDeviceToken() } } } @@ -107,7 +98,7 @@ class NotificationManager: Module, NotificationHandler, NotificationTokenHandler case .notDetermined: do { self.notificationsAuthorized = try await self.requestNotificationPermissions() - } catch let error as TimeoutError { + } catch _ as TimeoutError { self.state = .error(NotificationTokenTimeoutError()) } catch { self.state = .error(