diff --git a/Sources/SpeziAccount/AccountSetup.swift b/Sources/SpeziAccount/AccountSetup.swift index 11e3190..4586093 100644 --- a/Sources/SpeziAccount/AccountSetup.swift +++ b/Sources/SpeziAccount/AccountSetup.swift @@ -7,19 +7,10 @@ // import OrderedCollections +import SpeziViews import SwiftUI -public enum _AccountSetupState: EnvironmentKey, Sendable { // swiftlint:disable:this type_name - case generic - case setupShown - case requiringAdditionalInfo(_ keys: [any AccountKey.Type]) - case loadingExistingAccount - - public static let defaultValue: _AccountSetupState = .generic -} - - /// Login or signup for a user account. /// /// This view handles account setup for a user. It will show all enabled ``IdentityProvider``s from the configured ``AccountService``. @@ -64,9 +55,13 @@ public enum _AccountSetupState: EnvironmentKey, Sendable { // swiftlint:disable: /// /// ### Header /// - ``DefaultAccountSetupHeader`` +/// +/// ### Setup State +/// - ``SwiftUICore/EnvironmentValues/accountSetupState`` +/// - ``AccountSetupState`` @MainActor public struct AccountSetup: View { - private let setupCompleteClosure: (AccountDetails) -> Void + private let setupCompleteClosure: @MainActor (AccountDetails) async -> Void private let header: Header private let continueButton: Continue @@ -75,9 +70,10 @@ public struct AccountSetup: View { @Environment(\.followUpBehavior) private var followUpBehavior - @State private var setupState: _AccountSetupState = .generic + @State private var setupState: AccountSetupState = .presentingExistingAccount @State private var compliance: SignupProviderCompliance? - @State private var followUpSheet = false + @State private var presentFollowUpSheet = false + @State private var accountSetupTask: Task? private var hasSetupComponents: Bool { account.accountSetupComponents.contains { $0.configuration.isEnabled } @@ -86,51 +82,54 @@ public struct AccountSetup: View { public var body: some View { GeometryReader { proxy in ScrollView(.vertical) { - VStack { - if hasSetupComponents { - header - .environment(\._accountSetupState, setupState) - } - - Spacer() - - if let details = account.details, !details.isAnonymous { - switch setupState { - case let .requiringAdditionalInfo(keys): - followUpInformationSheet(details, requiredKeys: keys) - case .loadingExistingAccount: - // We allow the outer view to navigate away upon signup, before we show the existing account view - existingAccountLoading - default: - ExistingAccountView(details: details) { - continueButton - } - } - } else { - accountSetupView - .onAppear { - setupState = .setupShown - } - } - - Spacer() - Spacer() - Spacer() - } + scrollableContentView .padding(.horizontal, ViewSizing.outerHorizontalPadding) .frame(minHeight: proxy.size.height) .frame(maxWidth: .infinity) } } .onChange(of: [account.signedIn, account.details?.isAnonymous]) { - guard let details = account.details, - !details.isAnonymous, - case .setupShown = setupState else { + guard case .presentingSignup = setupState, + let details = account.details, + !details.isAnonymous else { return } handleSuccessfulSetup(details) } + .onDisappear { + accountSetupTask?.cancel() + } + } + + @ViewBuilder private var scrollableContentView: some View { + VStack { + if hasSetupComponents { + header + .environment(\.accountSetupState, setupState) + } + Spacer() + if let details = account.details, !details.isAnonymous { + switch setupState { + case let .requiringAdditionalInfo(keys): + followUpInformationSheet(details, requiredKeys: keys) + case .loadingExistingAccount, .presentingSignup: + ProgressView() + case .presentingExistingAccount: + ExistingAccountView(details: details) { + continueButton + } + } + } else { + accountSetupView + .onAppear { + setupState = .presentingSignup + } + } + Spacer() + Spacer() + Spacer() + } } @ViewBuilder private var accountSetupView: some View { @@ -163,17 +162,10 @@ public struct AccountSetup: View { } } } + - @ViewBuilder private var existingAccountLoading: some View { - ProgressView() - .task { - try? await Task.sleep(for: .seconds(2)) - setupState = .generic - } - } - - - fileprivate init(state: _AccountSetupState) where Header == DefaultAccountSetupHeader, Continue == EmptyView { + // for preview purposes + fileprivate init(state: AccountSetupState) where Header == DefaultAccountSetupHeader, Continue == EmptyView { self.setupCompleteClosure = { _ in } self.header = DefaultAccountSetupHeader() self.continueButton = EmptyView() @@ -189,7 +181,7 @@ public struct AccountSetup: View { /// - continue: A custom continue button you can place. This view will be rendered if the AccountSetup view is /// displayed with an already associated account. public init( - setupComplete: @escaping (AccountDetails) -> Void = { _ in }, + setupComplete: @MainActor @escaping (AccountDetails) async -> Void = { _ in }, @ViewBuilder header: () -> Header = { DefaultAccountSetupHeader() }, @ViewBuilder `continue`: () -> Continue = { EmptyView() } ) { @@ -202,19 +194,17 @@ public struct AccountSetup: View { @ViewBuilder private func followUpInformationSheet(_ details: AccountDetails, requiredKeys: [any AccountKey.Type]) -> some View { ProgressView() - .sheet(isPresented: $followUpSheet) { + .sheet(isPresented: $presentFollowUpSheet) { + // follow up information was completed! + handleSetupCompleted(details) + } content: { NavigationStack { FollowUpInfoSheet(keys: requiredKeys) } } .onAppear { - followUpSheet = true // we want full control through the setupState property - } - .onChange(of: followUpSheet) { - if !followUpSheet { // follow up information was completed! - setupState = .loadingExistingAccount - setupCompleteClosure(details) - } + // setupState made this view show up, therefore, automatically present the sheet + presentFollowUpSheet = true } } @@ -252,19 +242,19 @@ public struct AccountSetup: View { } private func handleSetupCompleted(_ details: AccountDetails) { - setupState = .loadingExistingAccount - setupCompleteClosure(details) - } -} + guard accountSetupTask == nil else { + return // already in progress + } + setupState = .loadingExistingAccount + accountSetupTask = Task { @MainActor in + defer { + accountSetupTask = nil + } -extension EnvironmentValues { - public var _accountSetupState: _AccountSetupState { // swiftlint:disable:this identifier_name missing_docs - get { - self[_AccountSetupState.self] - } - set { - self[_AccountSetupState.self] = newValue + await setupCompleteClosure(details) + try? await Task.sleep(for: .seconds(2)) + setupState = .presentingExistingAccount } } } diff --git a/Sources/SpeziAccount/Environment/AccountRequiredKey.swift b/Sources/SpeziAccount/Environment/AccountRequiredKey.swift index e5f2caa..e70dcd1 100644 --- a/Sources/SpeziAccount/Environment/AccountRequiredKey.swift +++ b/Sources/SpeziAccount/Environment/AccountRequiredKey.swift @@ -9,21 +9,9 @@ import SwiftUI -struct AccountRequiredKey: EnvironmentKey { - static let defaultValue = false -} - - extension EnvironmentValues { /// An environment variable that indicates if an account was configured to be required for the app. /// - /// Fore more information have a look at ``SwiftUICore/View/accountRequired(_:considerAnonymousAccounts:setupSheet:)``. - public var accountRequired: Bool { - get { - self[AccountRequiredKey.self] - } - set { - self[AccountRequiredKey.self] = newValue - } - } + /// Fore more information have a look at ``SwiftUICore/View/accountRequired(_:accountSetupIsComplete:setupSheet:)``. + @Entry public var accountRequired: Bool = false } diff --git a/Sources/SpeziAccount/Environment/AccountServiceConfiguration+Environment.swift b/Sources/SpeziAccount/Environment/AccountServiceConfiguration+Environment.swift index 69f357b..153c4fb 100644 --- a/Sources/SpeziAccount/Environment/AccountServiceConfiguration+Environment.swift +++ b/Sources/SpeziAccount/Environment/AccountServiceConfiguration+Environment.swift @@ -9,25 +9,10 @@ import SwiftUI -// swiftlint:disable:next type_name -private struct AccountServiceConfigurationEnvironmentKey: EnvironmentKey { - static var defaultValue: AccountServiceConfiguration { - .init(supportedKeys: .arbitrary) - } -} - - extension EnvironmentValues { /// Access the ``AccountServiceConfiguration`` within the context of a ``DataDisplayView`` or ``DataEntryView``. /// /// - Note: Accessing this environment value outside the view body of such view types, you will receive a unusable /// mock value. - public var accountServiceConfiguration: AccountServiceConfiguration { - get { - self[AccountServiceConfigurationEnvironmentKey.self] - } - set { - self[AccountServiceConfigurationEnvironmentKey.self] = newValue - } - } + @Entry public var accountServiceConfiguration: AccountServiceConfiguration = .init(supportedKeys: .arbitrary) } diff --git a/Sources/SpeziAccount/Environment/AccountViewType.swift b/Sources/SpeziAccount/Environment/AccountViewType.swift index e601d8f..7969022 100644 --- a/Sources/SpeziAccount/Environment/AccountViewType.swift +++ b/Sources/SpeziAccount/Environment/AccountViewType.swift @@ -58,20 +58,9 @@ extension AccountViewType: Sendable, Hashable {} extension EnvironmentValues { - private struct AccountViewTypeKey: EnvironmentKey { - static let defaultValue: AccountViewType? = nil - } - /// The type of `SpeziAccount` view a ``DataEntryView`` or ``DataDisplayView`` is placed in. /// /// ## Topics /// - ``AccountViewType`` - public var accountViewType: AccountViewType? { - get { - self[AccountViewTypeKey.self] - } - set { - self[AccountViewTypeKey.self] = newValue - } - } + @Entry public var accountViewType: AccountViewType? } diff --git a/Sources/SpeziAccount/Environment/FollowUpBehavior.swift b/Sources/SpeziAccount/Environment/FollowUpBehavior.swift index 35c5b11..a17b280 100644 --- a/Sources/SpeziAccount/Environment/FollowUpBehavior.swift +++ b/Sources/SpeziAccount/Environment/FollowUpBehavior.swift @@ -52,18 +52,7 @@ extension FollowUpBehavior: Sendable, Hashable {} extension EnvironmentValues { - private struct FollowUpBehaviorKey: EnvironmentKey { - static let defaultValue: FollowUpBehavior = .automatic - } - - var followUpBehavior: FollowUpBehavior { - get { - self[FollowUpBehaviorKey.self] - } - set { - self[FollowUpBehaviorKey.self] = newValue - } - } + @Entry var followUpBehavior: FollowUpBehavior = .automatic } diff --git a/Sources/SpeziAccount/Environment/PasswordFieldType.swift b/Sources/SpeziAccount/Environment/PasswordFieldType.swift index 7c17717..447430e 100644 --- a/Sources/SpeziAccount/Environment/PasswordFieldType.swift +++ b/Sources/SpeziAccount/Environment/PasswordFieldType.swift @@ -58,12 +58,5 @@ extension EnvironmentValues { /// ## Topics /// /// - ``PasswordFieldType`` - public var passwordFieldType: PasswordFieldType { - get { - self[PasswordFieldTypeKey.self] - } - set { - self[PasswordFieldTypeKey.self] = newValue - } - } + @Entry public var passwordFieldType: PasswordFieldType = .password } diff --git a/Sources/SpeziAccount/Environment/PreferredSetupStyle.swift b/Sources/SpeziAccount/Environment/PreferredSetupStyle.swift index f682b41..f03b3b7 100644 --- a/Sources/SpeziAccount/Environment/PreferredSetupStyle.swift +++ b/Sources/SpeziAccount/Environment/PreferredSetupStyle.swift @@ -29,19 +29,8 @@ public enum PreferredSetupStyle { extension EnvironmentValues { - private struct PreferredSetupStyleKey: EnvironmentKey { - static let defaultValue: PreferredSetupStyle = .automatic - } - /// The preferred style of presenting account setup views. - var preferredSetupStyle: PreferredSetupStyle { - get { - self[PreferredSetupStyleKey.self] - } - set { - self[PreferredSetupStyleKey.self] = newValue - } - } + @Entry var preferredSetupStyle: PreferredSetupStyle = .automatic } diff --git a/Sources/SpeziAccount/SpeziAccount.docc/SpeziAccount.md b/Sources/SpeziAccount/SpeziAccount.docc/SpeziAccount.md index 57eeaae..ed58efd 100644 --- a/Sources/SpeziAccount/SpeziAccount.docc/SpeziAccount.md +++ b/Sources/SpeziAccount/SpeziAccount.docc/SpeziAccount.md @@ -78,8 +78,9 @@ Refer to the article if you plan on impl - ``AccountOverview`` - ``AccountHeader`` - ``FollowUpInfoSheet`` -- ``SwiftUICore/View/accountRequired(_:considerAnonymousAccounts:setupSheet:)`` +- ``SwiftUICore/View/accountRequired(_:accountSetupIsComplete:setupSheet:)`` - ``SwiftUICore/EnvironmentValues/accountRequired`` +>>>>>>> main ### Environment & Preferences diff --git a/Sources/SpeziAccount/ViewModifier/AccountRequiredModifier.swift b/Sources/SpeziAccount/ViewModifier/AccountRequiredModifier.swift index 673a640..9576a9c 100644 --- a/Sources/SpeziAccount/ViewModifier/AccountRequiredModifier.swift +++ b/Sources/SpeziAccount/ViewModifier/AccountRequiredModifier.swift @@ -15,8 +15,8 @@ private let logger = Logger(subsystem: "edu.stanford.spezi.SpeziAccount", catego struct AccountRequiredModifier: ViewModifier { private let enabled: Bool + private let accountSetupIsComplete: (AccountDetails) -> Bool private let setupSheet: SetupSheet - private let considerAnonymousAccounts: Bool @Environment(Account.self) private var account: Account? // make sure that the modifier can be used when account is not configured @@ -32,15 +32,19 @@ struct AccountRequiredModifier: ViewModifier { return true // not signedIn } - // we present the sheet if the account is anonymous and we do not consider anonymous accounts to the fully signed in - return details.isAnonymous && !considerAnonymousAccounts + // we present the sheet if the account is not valid (yet) + return !accountSetupIsComplete(details) } - init(enabled: Bool, considerAnonymousAccounts: Bool, @ViewBuilder setupSheet: () -> SetupSheet) { + init( + enabled: Bool, + accountSetupIsComplete: @escaping (AccountDetails) -> Bool, + @ViewBuilder setupSheet: () -> SetupSheet + ) { self.enabled = enabled + self.accountSetupIsComplete = accountSetupIsComplete self.setupSheet = setupSheet() - self.considerAnonymousAccounts = considerAnonymousAccounts } @@ -58,7 +62,7 @@ struct AccountRequiredModifier: ViewModifier { guard account != nil else { logger.error(""" - accountRequired(_:considerAnonymousAccounts:setupSheet:) modifier was enabled but `Account` was not configured. \ + accountRequired(_:isValid:setupSheet:) modifier was enabled but `Account` was not configured. \ Make sure to include the `AccountConfiguration` the configuration section of your App delegate. """) return @@ -92,11 +96,41 @@ extension View { /// - setupSheet: The view that is presented if no account was detected. You may present the ``AccountSetup`` view here. /// This view is directly used with the standard SwiftUI sheet modifier. /// - Returns: The modified view. + @available(*, deprecated, renamed: "accountRequired(_:accountSetupIsComplete:setupSheet:)", message: "Please use the new closure-based modifier.") + @_disfavoredOverload // modifier with not parameters supplied should automatically choose the new one + @_documentation(visibility: internal) public func accountRequired( _ required: Bool = true, considerAnonymousAccounts: Bool = false, @ViewBuilder setupSheet: () -> SetupSheet ) -> some View { - modifier(AccountRequiredModifier(enabled: required, considerAnonymousAccounts: considerAnonymousAccounts, setupSheet: setupSheet)) + self.accountRequired(required, accountSetupIsComplete: { !$0.isAnonymous || considerAnonymousAccounts }, setupSheet: setupSheet) + } + + /// Use this modifier to ensure that there is always an associated account in your app. + /// + /// If account requirement is set, this modifier will automatically pop open an account setup sheet if + /// it is detected that the associated user account was removed. + /// + /// - Note: This modifier injects the ``SwiftUICore/EnvironmentValues/accountRequired`` property depending on the `required` argument. + /// + /// - Parameters: + /// - required: The flag indicating if an account is required at all times. + /// - accountSetupIsComplete: Determine if the account setup is complete. While the account setup is not completed, the `setupSheet` will continue to be presented. + /// - setupSheet: The view that is presented if no account was detected. You may present the ``AccountSetup`` view here. + /// This view is directly used with the standard SwiftUI sheet modifier. + /// - Returns: The modified view. + public func accountRequired( + _ required: Bool = true, + accountSetupIsComplete: @escaping (AccountDetails) -> Bool = { !$0.isAnonymous }, + @ViewBuilder setupSheet: () -> SetupSheet + ) -> some View { + modifier( + AccountRequiredModifier( + enabled: required, + accountSetupIsComplete: accountSetupIsComplete, + setupSheet: setupSheet + ) + ) } } diff --git a/Sources/SpeziAccount/Views/AccountSetup/AccountSetupState.swift b/Sources/SpeziAccount/Views/AccountSetup/AccountSetupState.swift new file mode 100644 index 0000000..fcac30f --- /dev/null +++ b/Sources/SpeziAccount/Views/AccountSetup/AccountSetupState.swift @@ -0,0 +1,110 @@ +// +// This source file is part of the Spezi open-source project +// +// SPDX-FileCopyrightText: 2023 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import SwiftUI + + +/// The state of the account setup process. +/// +/// Use the ``AccountSetupState`` type instead. +@available(*, deprecated, renamed: "AccountSetupState", message: "Please use the `AccountSetupState` type directly.") +public typealias _AccountSetupState = AccountSetupState // swiftlint:disable:this type_name + + +/// The state of the account setup process. +/// +/// This type models the different states of the ``AccountSetup`` view. You can retrieve this state using the +/// ``SwiftUICore/EnvironmentValues/accountSetupState`` environment variable in the `Header` view passed to `AccountSetup`. +public enum AccountSetupState { + /// The signup view is presented to the user. + case presentingSignup + /// Additional information is required from the user. + /// + /// The ``FollowUpInfoSheet`` is currently presented to the user. + case requiringAdditionalInfo(_ keys: [any AccountKey.Type]) + /// The existing account is currently being loaded. + /// + /// This state is entered while the `setupComplete` closure passed to ``AccountSetup/init(setupComplete:header:continue:)`` is getting executed. + case loadingExistingAccount + /// The currently associated account is presented. + /// + /// The user was already signed in or just got signed in successfully and is presented with their current user account. + case presentingExistingAccount + + /// The currently associated account is presented. + @available( + *, + deprecated, + renamed: "presentingExistingAccount", + message: "Please use the new `presentingExistingAccount` state or `isInSignup` property instead." + ) + public static var generic: AccountSetupState { + .presentingExistingAccount + } + + /// Setup is currently shown to the user. + @available(*, deprecated, renamed: "presentingSignup", message: "Please use the new `presentingSignup` state instead.") + public static var setupShown: AccountSetupState { + .presentingSignup + } + + /// Determine if the user is currently in the signup process. + public var isInSignup: Bool { + switch self { + case .presentingExistingAccount: + false + case .presentingSignup, .requiringAdditionalInfo, .loadingExistingAccount: + true + } + } + + /// Pattern matching operator. + /// - Parameters: + /// - lhs: The left hand side. + /// - rhs: The right hand side. + /// - Returns: Returns `true` if `lhs` and `rhs` both represent the same case. + public static func ~= (lhs: AccountSetupState, rhs: AccountSetupState) -> Bool { + // allow pattern matching for deprecated states + switch (lhs, rhs) { + case (.presentingSignup, .presentingSignup): + true + case (.requiringAdditionalInfo, .requiringAdditionalInfo): + true + case (.loadingExistingAccount, .loadingExistingAccount): + true + case (.presentingExistingAccount, .presentingExistingAccount): + true + default: + false + } + } +} + + +extension AccountSetupState: Sendable {} + + +extension EnvironmentValues { + /// The current account setup state. + /// + /// This environment property can be retrieved for child views of the ``AccountSetup`` view to determine the current setup state. + /// Use this in the `Header` view passed to ``AccountSetup/init(setupComplete:header:`continue`:)``. + @Entry public var accountSetupState: AccountSetupState = .presentingSignup + + /// The current account setup state. + @available(*, deprecated, renamed: "accountSetupState", message: "Please use the new `accountSetupState` directly.") + public var _accountSetupState: _AccountSetupState { + // swiftlint:disable:previous identifier_name + get { + accountSetupState + } + set { + accountSetupState = newValue + } + } +} diff --git a/Sources/SpeziAccount/Views/AccountSetup/DefaultAccountSetupHeader.swift b/Sources/SpeziAccount/Views/AccountSetup/DefaultAccountSetupHeader.swift index a25fafc..1babc2e 100644 --- a/Sources/SpeziAccount/Views/AccountSetup/DefaultAccountSetupHeader.swift +++ b/Sources/SpeziAccount/Views/AccountSetup/DefaultAccountSetupHeader.swift @@ -16,7 +16,7 @@ import SwiftUI public struct DefaultAccountSetupHeader: View { @Environment(Account.self) private var account - @Environment(\._accountSetupState) + @Environment(\.accountSetupState) private var setupState public var body: some View { @@ -29,7 +29,7 @@ public struct DefaultAccountSetupHeader: View { .padding(.top, 30) Group { - if account.signedIn, case .generic = setupState { + if account.signedIn, case .presentingExistingAccount = setupState { Text("ACCOUNT_WELCOME_SIGNED_IN_SUBTITLE", bundle: .module) } else { Text("ACCOUNT_WELCOME_SUBTITLE", bundle: .module) diff --git a/Sources/SpeziAccount/Views/AccountSetup/ExistingAccountView.swift b/Sources/SpeziAccount/Views/AccountSetup/ExistingAccountView.swift index 076f264..be2c8ad 100644 --- a/Sources/SpeziAccount/Views/AccountSetup/ExistingAccountView.swift +++ b/Sources/SpeziAccount/Views/AccountSetup/ExistingAccountView.swift @@ -21,26 +21,20 @@ struct ExistingAccountView: View { var body: some View { VStack { - ZStack { - VStack { - AccountSummaryBox(details: accountDetails) - Spacer() - .frame(maxHeight: 180) - } + AccountSummaryBox(details: accountDetails) + Spacer() - VStack { - Spacer() - continueButton - AsyncButton(.init("UP_LOGOUT", bundle: .atURL(from: .module)), role: .destructive, state: $viewState) { - try await account.accountService.logout() - } - .environment(\.defaultErrorDescription, .init("UP_LOGOUT_FAILED_DEFAULT_ERROR", bundle: .atURL(from: .module))) - .padding(8) - Spacer() - .frame(height: 20) - } + continueButton + + AsyncButton(.init("UP_LOGOUT", bundle: .atURL(from: .module)), role: .destructive, state: $viewState) { + try await account.accountService.logout() } + .environment(\.defaultErrorDescription, .init("UP_LOGOUT_FAILED_DEFAULT_ERROR", bundle: .atURL(from: .module))) + .padding(8) + Spacer() + .frame(height: 20) } + .padding(.top, 80) .viewStateAlert(state: $viewState) } diff --git a/Sources/SpeziAccount/Views/AccountSetup/FollowUpInfoSheet.swift b/Sources/SpeziAccount/Views/AccountSetup/FollowUpInfoSheet.swift index ec477ab..561281d 100644 --- a/Sources/SpeziAccount/Views/AccountSetup/FollowUpInfoSheet.swift +++ b/Sources/SpeziAccount/Views/AccountSetup/FollowUpInfoSheet.swift @@ -15,11 +15,13 @@ import SwiftUI struct FollowUpInfoFormHeader: View { var body: some View { - FormHeader( - image: Image(systemName: "person.crop.rectangle.badge.plus"), // swiftlint:disable:this accessibility_label_for_image - title: Text("FOLLOW_UP_INFORMATION_TITLE", bundle: .module), - instructions: Text("FOLLOW_UP_INFORMATION_INSTRUCTIONS", bundle: .module) - ) + FormHeader { + Image(systemName: "person.crop.rectangle.badge.plus") // swiftlint:disable:this accessibility_label_for_image + } title: { + Text("FOLLOW_UP_INFORMATION_TITLE", bundle: .module) + } instructions: { + Text("FOLLOW_UP_INFORMATION_INSTRUCTIONS", bundle: .module) + } } init() {} diff --git a/Sources/SpeziAccount/Views/AccountSummaryBox.swift b/Sources/SpeziAccount/Views/AccountSummaryBox.swift index c2d686d..c119fc6 100644 --- a/Sources/SpeziAccount/Views/AccountSummaryBox.swift +++ b/Sources/SpeziAccount/Views/AccountSummaryBox.swift @@ -15,6 +15,44 @@ struct AccountSummaryBox: View { private let model: AccountDisplayModel var body: some View { + FormHeader { + if let profileViewName = model.profileViewName { + UserProfileView(name: profileViewName) + .frame(height: 80) + } else { + Image(systemName: "person.crop.circle.fill") + .resizable() + .frame(maxWidth: 80, maxHeight: 80) +#if os(macOS) + .foregroundColor(Color(.systemGray)) +#else + .foregroundColor(Color(uiColor: .systemGray3)) +#endif + .accessibilityHidden(true) + } + } title: { + if let accountHeadline = model.accountHeadline { + Text(accountHeadline) + } else { + Text("Anonymous User", bundle: .module) + } + } instructions: { + if let subheadline = model.accountSubheadline { + Text(subheadline) + .font(.subheadline) + .foregroundColor(.secondary) + } + } + .padding() + .background( + RoundedRectangle(cornerRadius: 20) + .foregroundStyle(.regularMaterial) + // .shadow(color: .gray, radius: 2) + ) + .frame(maxWidth: ViewSizing.maxFrameWidth) + .accessibilityElement(children: .combine) + + /* HStack(spacing: 16) { Group { if let profileViewName = model.profileViewName { @@ -55,7 +93,7 @@ struct AccountSummaryBox: View { .shadow(color: .gray, radius: 2) ) .frame(maxWidth: ViewSizing.maxFrameWidth) - .accessibilityElement(children: .combine) + .accessibilityElement(children: .combine)*/ } /// Create a new `AccountSummaryBox` diff --git a/Sources/SpeziAccount/Views/SignupFormHeader.swift b/Sources/SpeziAccount/Views/SignupFormHeader.swift index 4503baa..c181b3f 100644 --- a/Sources/SpeziAccount/Views/SignupFormHeader.swift +++ b/Sources/SpeziAccount/Views/SignupFormHeader.swift @@ -9,10 +9,10 @@ import SwiftUI -struct FormHeader: View { +struct FormHeader: View { private let image: Image - private let title: Text - private let instructions: Text + private let title: Title + private let instructions: Instructions var body: some View { @@ -37,21 +37,23 @@ struct FormHeader: View { .frame(maxWidth: .infinity) } - init(image: Image, title: Text, instructions: Text) { - self.image = image - self.title = title - self.instructions = instructions + init(@ViewBuilder image: () -> Image, @ViewBuilder title: () -> Title, @ViewBuilder instructions: () -> Instructions) { + self.image = image() + self.title = title() + self.instructions = instructions() } } public struct SignupFormHeader: View { public var body: some View { - FormHeader( - image: Image(systemName: "person.fill.badge.plus"), // swiftlint:disable:this accessibility_label_for_image - title: Text("UP_SIGNUP_HEADER", bundle: .module), - instructions: Text("UP_SIGNUP_INSTRUCTIONS", bundle: .module) - ) + FormHeader { + Image(systemName: "person.fill.badge.plus") // swiftlint:disable:this accessibility_label_for_image + } title: { + Text("UP_SIGNUP_HEADER", bundle: .module) + } instructions: { + Text("UP_SIGNUP_INSTRUCTIONS", bundle: .module) + } } public init() {} diff --git a/Tests/UITests/TestApp/Features.swift b/Tests/UITests/TestApp/Features.swift index 4372c41..89c71e8 100644 --- a/Tests/UITests/TestApp/Features.swift +++ b/Tests/UITests/TestApp/Features.swift @@ -32,9 +32,7 @@ enum DefaultCredentials: String, ExpressibleByArgument { /// A collection of feature flags for the Test App. -struct Features: ParsableArguments, EnvironmentKey { - static let defaultValue = Features() - +struct Features: ParsableArguments { @Option(help: "Define which type of account services are used for the tests.") var serviceType: AccountServiceType = .mail @@ -53,12 +51,5 @@ struct Features: ParsableArguments, EnvironmentKey { extension EnvironmentValues { - var features: Features { - get { - self[Features.self] - } - set { - self[Features.self] = newValue - } - } + @Entry var features: Features = .init() } diff --git a/Tests/UITests/UITests.xcodeproj/project.pbxproj b/Tests/UITests/UITests.xcodeproj/project.pbxproj index f25fc7d..f62a076 100644 --- a/Tests/UITests/UITests.xcodeproj/project.pbxproj +++ b/Tests/UITests/UITests.xcodeproj/project.pbxproj @@ -718,7 +718,7 @@ /* Begin XCRemoteSwiftPackageReference section */ 2F027C9929D6C91D00234098 /* XCRemoteSwiftPackageReference "XCTestExtensions" */ = { isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/StanfordBDHG/XCTestExtensions"; + repositoryURL = "https://github.com/StanfordBDHG/XCTestExtensions.git"; requirement = { kind = upToNextMajorVersion; minimumVersion = 1.0.0;