From 2f88e04a66bb3f1444763f7e3c103353b9be6f77 Mon Sep 17 00:00:00 2001
From: Andi <andreas.bauer@stanford.edu>
Date: Mon, 23 Oct 2023 11:13:19 -0700
Subject: [PATCH] Infrastructure changes to allow for seamless Single-Sign-On
 implementations. (#28)

# Infrastructure changes to allow for seamless Single-Sign-On
implementations.

## :recycle: Current situation & Problem
While `IdentityProvider` are currently supported by `SpeziAccount`,
there are some issues when trying to accommodate more extended use cases
or certain limitations when combining different functionality.

Below is a list of certain pain points including general improvements
this PR makes:
* Currently, account values requirements are globally applied. E.g., a
configured password key is required for **all** account services. This
doesn't fit very well for SSO providers as they intentionally don't
collect passwords. Instead one would need a way of declaring AccountKeys
required in the context of a given account services. This is resolved by
changing the effect of the `RequiredAccountKeys` configuration an
account service would provide and consequentially removing the need for
the user to declare the `password` account value globally required.
* We added a new `FollowUpInfoSheet` that automatically pops up after
account setup to assist the user to provide any additional account
details that are configured to be required. That is especially useful
when using Identity Provider like Sign in with Apple that only provide a
fixed set of user information.
A new `verifyRequiredAccountDetails(_:)` modifier was added to implement
the same logic on the global application level. This can be used that
existing users always are in line with the latest configuration of your
app.
* The `accountRequired(_:setupSheet:)` modifier was added to enforce a
user account at all times.
* Added a new globally unique and stable `AccountId`, mandatory for each
user account.
* Fixed an issue where the SignupForm cancel confirmation would always
pop up when using AccountKeys with default values.
* Other fixes and improvements.

## :gear: Release Notes
Added several infrastructure enhancements that allow for an improved
user experience and compatibility when using identity providers.

### Breaking Changes
* We introduce a new `accountId` (see `AccountIdKey`) account value that
is mandatory for all user accounts. This is now also used as a primary
identifier for `AccountStorageStandard`s.

## :books: Documentation
Documentation was updated respectively.


## :white_check_mark: Testing
Test cases were added for new functionality or fixed functionality.

## :pencil: Code of Conduct & Contributing Guidelines

By submitting creating this pull request, you agree to follow our [Code
of
Conduct](https://github.com/StanfordSpezi/.github/blob/main/CODE_OF_CONDUCT.md)
and [Contributing
Guidelines](https://github.com/StanfordSpezi/.github/blob/main/CONTRIBUTING.md):
- [x] I agree to follow the [Code of
Conduct](https://github.com/StanfordSpezi/.github/blob/main/CODE_OF_CONDUCT.md)
and [Contributing
Guidelines](https://github.com/StanfordSpezi/.github/blob/main/CONTRIBUTING.md).

---------

Co-authored-by: Paul Schmiedmayer <PSchmiedmayer@users.noreply.github.com>
---
 Sources/SpeziAccount/Account.swift            |  38 ++++-
 .../SpeziAccount/AccountConfiguration.swift   |  43 ++---
 Sources/SpeziAccount/AccountHeader.swift      |  61 ++++++-
 Sources/SpeziAccount/AccountOverview.swift    |   4 +-
 .../AccountService/AccountService.swift       |   7 +-
 .../Configuration/RequiredAccountKeys.swift   |  12 +-
 .../Configuration/SupportedAccountKeys.swift  |   2 +-
 .../Wrapper/StandardBacked.swift              |   4 +
 .../StorageStandardBackedAccountService.swift |  39 +++--
 Sources/SpeziAccount/AccountSetup.swift       |  86 +++++++---
 .../SpeziAccount/AccountStorageStandard.swift |   6 +-
 .../AccountValue/AccountKey+Views.swift       |  13 +-
 .../AccountValue/AccountKey.swift             |   7 +
 .../AccountValue/AccountKeyCollection.swift   |   4 +
 .../Collections/AccountDetails.swift          |   4 +
 .../Collections/ModifiedAccountDetails.swift  |  13 ++
 .../Collections/SignupDetails.swift           |   4 +-
 ...rror.swift => AccountOperationError.swift} |  19 ++-
 .../AccountValueConfiguration.swift           |  43 +++++
 .../AccountValue/Keys/AccountIdKey.swift      |  73 +++++++++
 .../AccountValue/Keys/IsNewUserKey.swift      |  21 +++
 .../AccountValue/Keys/PasswordKey.swift       |   4 +-
 .../AccountValue/Keys/UserIdKey.swift         |  25 ++-
 .../Environment/AccountRequiredKey.swift      |  29 ++++
 .../MockUserIdPasswordAccountService.swift    |   6 +
 .../Model/AdditionalRecordId.swift            |  14 +-
 .../Model/Validation/ValidationEngines.swift  |  16 +-
 .../Resources/de.lproj/Localizable.strings    |  31 ++--
 .../Resources/en.lproj/Localizable.strings    |  27 ++-
 .../Creating your own Account Service.md      |   6 +-
 .../Setup Guides/Initial Setup.md             |   5 +-
 .../AccountOverviewFormViewModel.swift        |  62 +++++--
 .../AccountRequiredModifier.swift             |  69 ++++++++
 ...VerifyRequiredAccountDetailsModifier.swift |  78 +++++++++
 ...tRow.swift => AccountKeyOverviewRow.swift} |   8 +-
 .../AccountOverviewHeader.swift               |   5 +-
 .../AccountOverviewSections.swift             |  42 +++--
 .../Views/AccountOverview/NameOverview.swift  |  50 +++---
 .../AccountOverview/SecurityOverview.swift    |  33 ++--
 .../AccountOverview/SingleEditView.swift      |   2 +-
 .../DefaultAccountSetupHeader.swift           |   6 +-
 .../AccountSetup/FollowUpInfoSheet.swift      | 155 ++++++++++++++++++
 .../AccountSetup/SignupSectionsView.swift     |  73 +++++++++
 .../Views/AccountSummaryBox.swift             |   5 +-
 .../DataEntry/GeneralizedDataEntryView.swift  |  14 +-
 Sources/SpeziAccount/Views/SignupForm.swift   |  88 +++++-----
 .../AccountTests/AccountTestsView.swift       |  72 +++++---
 .../TestAccountConfiguration.swift            |   6 +-
 .../AccountTests/TestAccountService.swift     |  17 +-
 .../TestApp/AccountTests/UserStorage.swift    |  11 +-
 Tests/UITests/TestApp/Features.swift          |   7 +
 Tests/UITests/TestApp/TestApp.entitlements    |  10 ++
 .../TestApp/TestApp.entitlements.license      |   5 +
 Tests/UITests/TestApp/TestAppDelegate.swift   |  17 +-
 .../TestAppUITests/AccountOverviewTests.swift |  30 +++-
 .../TestAppUITests/AccountSetupTests.swift    |  39 ++++-
 .../TestAppUITests/Utils/SignupView.swift     |   8 +-
 .../TestAppUITests/Utils/TestApp.swift        |  10 +-
 .../UITests/UITests.xcodeproj/project.pbxproj |   4 +
 .../xcshareddata/xcschemes/TestApp.xcscheme   |   8 +
 60 files changed, 1260 insertions(+), 340 deletions(-)
 rename Sources/SpeziAccount/AccountValue/Configuration/{AccountValueConfigurationError.swift => AccountOperationError.swift} (67%)
 create mode 100644 Sources/SpeziAccount/AccountValue/Keys/AccountIdKey.swift
 create mode 100644 Sources/SpeziAccount/AccountValue/Keys/IsNewUserKey.swift
 create mode 100644 Sources/SpeziAccount/Environment/AccountRequiredKey.swift
 create mode 100644 Sources/SpeziAccount/ViewModifier/AccountRequiredModifier.swift
 create mode 100644 Sources/SpeziAccount/ViewModifier/VerifyRequiredAccountDetailsModifier.swift
 rename Sources/SpeziAccount/Views/AccountOverview/{AccountKeyEditRow.swift => AccountKeyOverviewRow.swift} (92%)
 create mode 100644 Sources/SpeziAccount/Views/AccountSetup/FollowUpInfoSheet.swift
 create mode 100644 Sources/SpeziAccount/Views/AccountSetup/SignupSectionsView.swift
 create mode 100644 Tests/UITests/TestApp/TestApp.entitlements
 create mode 100644 Tests/UITests/TestApp/TestApp.entitlements.license

diff --git a/Sources/SpeziAccount/Account.swift b/Sources/SpeziAccount/Account.swift
index 1141214a..3889e51f 100644
--- a/Sources/SpeziAccount/Account.swift
+++ b/Sources/SpeziAccount/Account.swift
@@ -51,7 +51,7 @@ import SwiftUI
 /// ### Managing Account state
 /// This section provides an overview on how to manage and manipulate the current user account as an ``AccountService``.
 ///
-/// - ``supplyUserDetails(_:)``
+/// - ``supplyUserDetails(_:isNewUser:)``
 /// - ``removeUserDetails()``
 ///
 /// ### Initializers for your Preview Provider
@@ -89,7 +89,7 @@ public class Account: ObservableObject, Sendable {
     ///
     /// - Note: This array also contains ``IdentityProvider``s that need to be treated differently due to differing
     ///     ``AccountSetupViewStyle`` implementations (see ``IdentityProviderViewStyle``).
-    let registeredAccountServices: [any AccountService]
+    public let registeredAccountServices: [any AccountService]
 
     /// Initialize a new `Account` object by providing all properties individually.
     /// - Parameters:
@@ -112,8 +112,8 @@ public class Account: ObservableObject, Sendable {
             logger.warning(
                 """
                 Your AccountConfiguration doesn't have the \\.userId (aka. UserIdKey) configured. \
-                A primary and unique user identifier is expected with most SpeziAccount components and \
-                will result in those components breaking.
+                A primary, user-visible identifier is recommended with most SpeziAccount components for \
+                an optimal user experience. Ignore this warning if you know what you are doing.
                 """
             )
         }
@@ -183,10 +183,30 @@ public class Account: ObservableObject, Sendable {
     /// This method is called by the ``AccountService`` every time the state of the user account changes.
     /// Either if the went from no logged in user to having a logged in user, or if the details of the user account changed.
     ///
-    /// - Parameter details: The ``AccountDetails`` of the currently logged in user account.
-    public func supplyUserDetails(_ details: AccountDetails) async throws {
+    /// - Parameters:
+    ///   - details: The ``AccountDetails`` of the currently logged in user account.
+    ///   - isNewUser: An optional flag that indicates if the provided account details are for a new user registration.
+    ///     If this flag is set to `true`, the ``AccountSetup`` view will render a additional information sheet not only for
+    ///     ``AccountKeyRequirement/required``, but also for ``AccountKeyRequirement/collected`` account values.
+    ///     This is primarily helpful for identity providers. You might not want to set this flag
+    ///     if you using the builtin ``SignupForm``!
+    public func supplyUserDetails(_ details: AccountDetails, isNewUser: Bool = false) async throws {
+        precondition(
+            details.contains(AccountIdKey.self),
+            """
+            The provided `AccountDetails` do not have the \\.accountId (aka. AccountIdKey) set. \
+            A primary, unique and stable user identifier is expected with most SpeziAccount components and \
+            will result in those components breaking.
+            """
+        )
+
         var details = details
 
+        if isNewUser {
+            details.patchIsNewUser(true)
+        }
+
+
         // Account details will always get built by the respective Account Service. Therefore, we need to patch it
         // if they are wrapped into a StandardBacked one such that the `AccountDetails` carry the correct reference.
         for service in registeredAccountServices {
@@ -206,7 +226,9 @@ public class Account: ObservableObject, Sendable {
 
         if let standardBacked = details.accountService as? any StandardBacked,
            let storageStandard = standardBacked.standard as? any AccountStorageStandard {
-            let recordId = AdditionalRecordId(serviceId: standardBacked.backedId, userId: details.userId)
+            let recordId = AdditionalRecordId(serviceId: standardBacked.backedId, accountId: details.accountId)
+
+            try await standardBacked.preUserDetailsSupply(recordId: recordId)
 
             let unsupportedKeys = details.accountService.configuration
                 .unsupportedAccountKeys(basedOn: configuration)
@@ -232,7 +254,7 @@ public class Account: ObservableObject, Sendable {
         if let details,
            let standardBacked = details.accountService as? any StandardBacked,
            let storageStandard = standardBacked.standard as? any AccountStorageStandard {
-            let recordId = AdditionalRecordId(serviceId: standardBacked.backedId, userId: details.userId)
+            let recordId = AdditionalRecordId(serviceId: standardBacked.backedId, accountId: details.accountId)
 
             await storageStandard.clear(recordId)
         }
diff --git a/Sources/SpeziAccount/AccountConfiguration.swift b/Sources/SpeziAccount/AccountConfiguration.swift
index 6c3cc6f6..c7229bbc 100644
--- a/Sources/SpeziAccount/AccountConfiguration.swift
+++ b/Sources/SpeziAccount/AccountConfiguration.swift
@@ -77,9 +77,6 @@ public final class AccountConfiguration: Component, ObservableObjectProvider {
     public func configure() {
         // assemble the final array of account services
         let accountServices = (providedAccountServices + self.accountServices).map { service in
-            // verify that the configuration matches what is expected by the account service
-            verifyAccountServiceRequirements(of: service)
-
             // Verify account service can store all configured account keys.
             // If applicable, wraps the service into an StandardBackedAccountService
             let service = verifyConfigurationRequirements(against: service)
@@ -99,36 +96,22 @@ public final class AccountConfiguration: Component, ObservableObjectProvider {
         self.account?.injectWeakAccount(into: standard)
     }
 
-    private func verifyAccountServiceRequirements(of service: any AccountService) {
-        let requiredValues = service.configuration.requiredAccountKeys
-
-        // A collection of AccountKey.Type which aren't configured by the user or not configured to be required
-        // but the Account Service requires them.
-        let mismatchedKeys: [any AccountKeyWithDescription] = requiredValues.filter { keyWithDescription in
-            let key = keyWithDescription.key
-            let configuration = configuredAccountKeys[key]
-            return configuration == nil
-                || (key.isRequired && configuration?.requirement != .required)
-        }
-
-        guard !mismatchedKeys.isEmpty else {
-            return
-        }
-
-        // Note: AccountKeyWithDescription has a nice `debugDescription` that pretty prints the KeyPath property name
-        preconditionFailure(
-            """
-            You configured the AccountService \(service) which requires the following account values to be configured: \
-            \(mismatchedKeys.description).
-
-            Please modify your `AccountServiceConfiguration` to have these account values configured.
-            """
-        )
-    }
-
     private func verifyConfigurationRequirements(against service: any AccountService) -> any AccountService {
         logger.debug("Checking \(service.description) against the configured account keys.")
 
+        // if account service states exact supported keys, AccountIdKey must be one of them
+        if case let .exactly(keys) = service.configuration.supportedAccountKeys {
+            precondition(
+                keys.contains(AccountIdKey.self),
+                """
+                The account service \(type(of: service)) doesn't have the \\.accountId (aka. AccountIdKey) configured \
+                as an supported key. \
+                A primary, unique and stable user identifier is expected with most SpeziAccount components and \
+                will result in those components breaking.
+                """
+            )
+        }
+
         // collect all values that cannot be handled by the account service
         let unmappedAccountKeys: [any AccountKeyConfiguration] = service.configuration
             .unsupportedAccountKeys(basedOn: configuredAccountKeys)
diff --git a/Sources/SpeziAccount/AccountHeader.swift b/Sources/SpeziAccount/AccountHeader.swift
index b5d20b28..262dfa93 100644
--- a/Sources/SpeziAccount/AccountHeader.swift
+++ b/Sources/SpeziAccount/AccountHeader.swift
@@ -10,7 +10,7 @@ import SpeziViews
 import SwiftUI
 
 
-/// A summary view for ``SpeziAccountOverview`` that can be used as a Button to link to ``SpeziAccountOverview``.
+/// A account summary view that can be used to link to the ``AccountOverview``.
 ///
 /// Below is a short code example on how to use the `AccountHeader` view.
 ///
@@ -37,7 +37,8 @@ public struct AccountHeader: View {
     public enum Defaults {
         /// Default caption.
         @_documentation(visibility: internal)
-        public static let caption = LocalizedStringResource("ACCOUNT_HEADER_CAPTION", bundle: .atURL(from: .module)) // swiftlint:disable:this attributes
+        public static let caption = LocalizedStringResource("ACCOUNT_HEADER_CAPTION", bundle: .atURL(from: .module))
+        // swiftlint:disable:previous attributes
     }
     
     @EnvironmentObject private var account: Account
@@ -47,12 +48,23 @@ public struct AccountHeader: View {
         let accountDetails = account.details
         
         HStack {
-            UserProfileView(name: accountDetails?.name ?? PersonNameComponents(givenName: "Placeholder", familyName: "Placeholder"))
-                .frame(height: 60)
-                .redacted(reason: account.details == nil ? .placeholder : [])
-                .accessibilityHidden(true)
+            if let accountDetails,
+               let name = accountDetails.name {
+                UserProfileView(name: name)
+                    .frame(height: 60)
+                    .accessibilityHidden(true)
+            } else {
+                Image(systemName: "person.crop.circle.fill")
+                    .resizable()
+                    .frame(width: 60, height: 60)
+                    .foregroundColor(Color(.systemGray3))
+                    .accessibilityHidden(true)
+            }
+
             VStack(alignment: .leading) {
-                Text(accountDetails?.name?.formatted() ?? "Placeholder")
+                let nameTitle = accountDetails?.name?.formatted(.name(style: .long)) ?? accountDetails?.userId ?? "Placeholder"
+
+                Text(nameTitle)
                     .font(.title2)
                     .fontWeight(.semibold)
                     .redacted(reason: account.details == nil ? .placeholder : [])
@@ -89,7 +101,7 @@ public struct AccountHeader: View {
     let details = AccountDetails.Builder()
         .set(\.userId, value: "andi.bauer@tum.de")
         .set(\.name, value: PersonNameComponents(givenName: "Andreas", familyName: "Bauer"))
-    
+
     return NavigationStack {
         Form {
             Section {
@@ -103,4 +115,37 @@ public struct AccountHeader: View {
     }
         .environmentObject(Account(building: details, active: MockUserIdPasswordAccountService()))
 }
+
+#Preview {
+    let details = AccountDetails.Builder()
+        .set(\.userId, value: "andi.bauer@tum.de")
+
+    return NavigationStack {
+        Form {
+            Section {
+                NavigationLink {
+                    AccountOverview()
+                } label: {
+                    AccountHeader()
+                }
+            }
+        }
+    }
+    .environmentObject(Account(building: details, active: MockUserIdPasswordAccountService()))
+}
+
+#Preview {
+    NavigationStack {
+        Form {
+            Section {
+                NavigationLink {
+                    AccountOverview()
+                } label: {
+                    AccountHeader()
+                }
+            }
+        }
+    }
+        .environmentObject(Account(MockUserIdPasswordAccountService()))
+}
 #endif
diff --git a/Sources/SpeziAccount/AccountOverview.swift b/Sources/SpeziAccount/AccountOverview.swift
index a71749b2..8ec97738 100644
--- a/Sources/SpeziAccount/AccountOverview.swift
+++ b/Sources/SpeziAccount/AccountOverview.swift
@@ -10,7 +10,7 @@ import SpeziViews
 import SwiftUI
 
 
-/// The essential ``SpeziAccount`` view to view and modify the active account details.
+/// The essential `SpeziAccount` view to view and modify the active account details.
 ///
 /// This provides an overview of the current account details. Further, it allows the user to modify their
 /// account values.
@@ -52,7 +52,7 @@ import SwiftUI
 /// }
 /// ```
 ///
-/// - Note: The ``init(isEditing:)`` initializer allows to pass an optional `Bool` Binding to retrieve the
+/// - Note: The ``init(isEditing:additionalSections:)`` initializer allows to pass an optional `Bool` Binding to retrieve the
 ///     current edit mode of the view. This can be helpful to, e.g., render a custom `Close` Button if the
 ///     view is not editing when presenting the AccountOverview in a sheet.
 public struct AccountOverview<AdditionalSections: View>: View {
diff --git a/Sources/SpeziAccount/AccountService/AccountService.swift b/Sources/SpeziAccount/AccountService/AccountService.swift
index b188dd22..cb10197f 100644
--- a/Sources/SpeziAccount/AccountService/AccountService.swift
+++ b/Sources/SpeziAccount/AccountService/AccountService.swift
@@ -19,8 +19,9 @@ import SwiftUI
 /// You may improve the user experience or rely on user interface defaults if you adopt protocols like
 /// ``EmbeddableAccountService`` or ``UserIdPasswordAccountService``.
 ///
-/// - Note: `SpeziAccount` provides the generalized ``UserIdKey`` unique user identifier that can be customized
-///     using the ``UserIdConfiguration``.
+/// - Important: Every user account is expected to have a primary and unique user identifier.
+///     SpeziAccount requires a stable and internal ``AccountIdKey`` unique user identifier and offers
+///     a user visible ``UserIdKey`` which can be customized using the ``UserIdConfiguration``.
 ///
 /// You can learn more about creating an account service at: <doc:Creating-your-own-Account-Service>.
 ///
@@ -52,7 +53,7 @@ public protocol AccountService: AnyObject, Hashable, CustomStringConvertible, Se
 
     /// Create a new user account for the provided ``SignupDetails``.
     ///
-    /// - Note: You must call ``Account/supplyUserDetails(_:)`` eventually once the user context was established after this call.
+    /// - Note: You must call ``Account/supplyUserDetails(_:isNewUser:)`` eventually once the user context was established after this call.
     /// - Parameter signupDetails: The signup details
     /// - Throws: Throw an `Error` type conforming to `LocalizedError` if the signup operation was unsuccessful,
     ///     inorder to present a localized description to the user.
diff --git a/Sources/SpeziAccount/AccountService/Configuration/RequiredAccountKeys.swift b/Sources/SpeziAccount/AccountService/Configuration/RequiredAccountKeys.swift
index ceea4bfc..729d8435 100644
--- a/Sources/SpeziAccount/AccountService/Configuration/RequiredAccountKeys.swift
+++ b/Sources/SpeziAccount/AccountService/Configuration/RequiredAccountKeys.swift
@@ -9,18 +9,16 @@
 import Spezi
 
 
-/// The collection of ``AccountKey``s that are required to use the associated ``AccountService``.
+/// The collection of ``AccountKey``s that are required when using the associated ``AccountService``.
 ///
 /// A ``AccountService`` may set this configuration to communicate that a certain set of ``AccountKey``s are
-/// required to be configured in the ``AccountValueConfiguration`` provided in the ``AccountConfiguration`` in
-/// order to user the account service.
-///
-/// Upon startup, `SpeziAccount` automatically verifies that the user-configured account values match the expectation
-/// set by the ``AccountService`` through this configuration option.
+/// required to use the given account service. For example, a password-based account service defines the password
+/// to be required using this configuration as the account value is only required in the context of using this specific
+/// account service.
 ///
 /// Access the configuration via the ``AccountServiceConfiguration/requiredAccountKeys``.
 ///
-/// Below is an example on how to provide this option.
+/// Below is an example configuration for a userid-password-based account service.
 ///
 /// ```swift
 /// let configuration = AccountServiceConfiguration(/* ... */) {
diff --git a/Sources/SpeziAccount/AccountService/Configuration/SupportedAccountKeys.swift b/Sources/SpeziAccount/AccountService/Configuration/SupportedAccountKeys.swift
index 55aa4caa..8707a119 100644
--- a/Sources/SpeziAccount/AccountService/Configuration/SupportedAccountKeys.swift
+++ b/Sources/SpeziAccount/AccountService/Configuration/SupportedAccountKeys.swift
@@ -18,7 +18,7 @@
 ///
 /// Access the configuration via the ``AccountServiceConfiguration/supportedAccountKeys``.
 ///
-/// Belo is an example on how to provide a fixed set of supported account keys.
+/// Below is an example on how to provide a fixed set of supported account keys.
 ///
 /// ```swift
 /// let supportedKeys = AccountKeyCollection {
diff --git a/Sources/SpeziAccount/AccountService/Wrapper/StandardBacked.swift b/Sources/SpeziAccount/AccountService/Wrapper/StandardBacked.swift
index 694c9c99..adfa12b4 100644
--- a/Sources/SpeziAccount/AccountService/Wrapper/StandardBacked.swift
+++ b/Sources/SpeziAccount/AccountService/Wrapper/StandardBacked.swift
@@ -20,6 +20,8 @@ protocol StandardBacked: AccountService {
     init(service: Service, standard: AccountStandard)
 
     func isBacking(service accountService: any AccountService) -> Bool
+
+    func preUserDetailsSupply(recordId: AdditionalRecordId) async throws
 }
 
 
@@ -39,6 +41,8 @@ extension StandardBacked {
         }
         return self.accountService.objId == service.objId
     }
+
+    func preUserDetailsSupply(recordId: AdditionalRecordId) async throws {}
 }
 
 
diff --git a/Sources/SpeziAccount/AccountService/Wrapper/StorageStandardBackedAccountService.swift b/Sources/SpeziAccount/AccountService/Wrapper/StorageStandardBackedAccountService.swift
index 4bf35f82..ded588b0 100644
--- a/Sources/SpeziAccount/AccountService/Wrapper/StorageStandardBackedAccountService.swift
+++ b/Sources/SpeziAccount/AccountService/Wrapper/StorageStandardBackedAccountService.swift
@@ -18,6 +18,8 @@ actor StorageStandardBackedAccountService<Service: AccountService, Standard: Acc
     let standard: Standard
     let serviceSupportedKeys: AccountKeyCollection
 
+    private var pendingSignupDetails: SignupDetails?
+
     nonisolated var configuration: AccountServiceConfiguration {
         accountService.configuration
     }
@@ -27,9 +29,9 @@ actor StorageStandardBackedAccountService<Service: AccountService, Standard: Acc
     }
 
 
-    private var currentUserId: String? {
+    private var currentAccountId: String? {
         get async {
-            await account.details?.userId
+            await account.details?.accountId
         }
     }
 
@@ -44,24 +46,28 @@ actor StorageStandardBackedAccountService<Service: AccountService, Standard: Acc
         self.serviceSupportedKeys = keys
     }
 
-
     func signUp(signupDetails: SignupDetails) async throws {
         let details = splitDetails(from: signupDetails)
 
-        let recordId = AdditionalRecordId(serviceId: accountService.id, userId: signupDetails.userId)
-
-        // call standard first, such that it will happen before any `supplyAccountDetails` calls made by the Account Service
-        try await standard.create(recordId, details.standard)
+        // save the details until the accountId is available. This will be in preUserDetailsSupply
+        self.pendingSignupDetails = details.standard
 
         try await accountService.signUp(signupDetails: details.service)
     }
 
+    func preUserDetailsSupply(recordId: AdditionalRecordId) async throws {
+        if let pendingSignupDetails {
+            try await standard.create(recordId, pendingSignupDetails)
+            self.pendingSignupDetails = nil
+        }
+    }
+
     func updateAccountDetails(_ modifications: AccountModifications) async throws {
-        guard let userId = await currentUserId else {
+        guard let accountId = await currentAccountId else {
             return
         }
 
-        let modifiedDetails = splitDetails(from: modifications.modifiedDetails, copyUserId: true)
+        let modifiedDetails = splitDetails(from: modifications.modifiedDetails)
         let removedDetails = splitDetails(from: modifications.removedAccountDetails)
 
         let serviceModifications = AccountModifications(
@@ -74,7 +80,7 @@ actor StorageStandardBackedAccountService<Service: AccountService, Standard: Acc
             removedAccountDetails: removedDetails.standard
         )
 
-        let recordId = AdditionalRecordId(serviceId: accountService.id, userId: userId)
+        let recordId = AdditionalRecordId(serviceId: accountService.id, accountId: accountId)
 
         // first call the standard, such that it will happen before any `supplyAccountDetails` calls made by the Account Service
         try await standard.modify(recordId, standardModifications)
@@ -83,29 +89,22 @@ actor StorageStandardBackedAccountService<Service: AccountService, Standard: Acc
     }
 
     func delete() async throws {
-        guard let userId = await currentUserId else {
+        guard let accountId = await currentAccountId else {
             return
         }
 
-        try await standard.delete(AdditionalRecordId(serviceId: accountService.id, userId: userId))
+        try await standard.delete(AdditionalRecordId(serviceId: accountService.id, accountId: accountId))
         try await accountService.delete()
     }
 
     private func splitDetails<Values: AccountValues>(
-        from details: Values,
-        copyUserId: Bool = false
+        from details: Values
     ) -> (service: Values, standard: Values) {
         let serviceBuilder = AccountValuesBuilder<Values>()
         let standardBuilder = AccountValuesBuilder<Values>(from: details)
 
 
         for element in serviceSupportedKeys {
-            if copyUserId && element.key == UserIdKey.self {
-                // ensure that in a `modify` call, the Standard gets notified about the updated userId as the primary
-                // identifier will change.
-                continue
-            }
-
             // remove all service supported keys from the standard builder (which is a copy of `details` currently)
             standardBuilder.remove(element.key)
         }
diff --git a/Sources/SpeziAccount/AccountSetup.swift b/Sources/SpeziAccount/AccountSetup.swift
index 80444081..06b37b82 100644
--- a/Sources/SpeziAccount/AccountSetup.swift
+++ b/Sources/SpeziAccount/AccountSetup.swift
@@ -12,12 +12,13 @@ import SwiftUI
 public enum _AccountSetupState: EnvironmentKey { // swiftlint:disable:this type_name
     case generic
     case setupShown
+    case requiringAdditionalInfo(_ keys: [any AccountKey.Type])
     case loadingExistingAccount
 
     public static var defaultValue: _AccountSetupState = .generic
 }
 
-/// The essential ``SpeziAccount`` view to login into or signup for a user account.
+/// The essential `SpeziAccount` view to login into or signup for a user account.
 ///
 /// This view handles account setup for a user. The user can choose from all configured ``AccountService`` and
 /// ``IdentityProvider`` instances to setup an active user account. They might create a new account with a given
@@ -53,12 +54,14 @@ public enum _AccountSetupState: EnvironmentKey { // swiftlint:disable:this type_
 /// }
 /// ```
 public struct AccountSetup<Header: View, Continue: View>: View {
+    private let setupCompleteClosure: (AccountDetails) -> Void
     private let header: Header
     private let continueButton: Continue
 
     @EnvironmentObject var account: Account
 
     @State private var setupState: _AccountSetupState = .generic
+    @State private var followUpSheet = false
 
     private var services: [any AccountService] {
         account.registeredAccountServices
@@ -82,14 +85,13 @@ public struct AccountSetup<Header: View, Continue: View>: View {
                     Spacer()
 
                     if let details = account.details {
-                        if case .loadingExistingAccount = setupState {
+                        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
-                            ProgressView()
-                                .task {
-                                    try? await Task.sleep(for: .seconds(2))
-                                    setupState = .generic
-                                }
-                        } else {
+                            existingAccountLoading
+                        default:
                             ExistingAccountView(details: details) {
                                 continueButton
                             }
@@ -110,9 +112,16 @@ public struct AccountSetup<Header: View, Continue: View>: View {
                     .frame(maxWidth: .infinity)
             }
         }
-            .onReceive(account.$signedIn) { signedIn in
-                if signedIn, case .setupShown = setupState {
-                    setupState = .loadingExistingAccount
+            .onReceive(account.$details) { details in
+                if let details, case .setupShown = setupState {
+                    let missingKeys = account.configuration.missingRequiredKeys(for: details, includeCollected: details.isNewUser)
+
+                    if missingKeys.isEmpty {
+                        setupState = .loadingExistingAccount
+                        setupCompleteClosure(details)
+                    } else {
+                        setupState = .requiringAdditionalInfo(missingKeys)
+                    }
                 }
             }
     }
@@ -136,26 +145,59 @@ public struct AccountSetup<Header: View, Continue: View>: 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 {
+        self.setupCompleteClosure = { _ in }
         self.header = DefaultAccountSetupHeader()
         self.continueButton = EmptyView()
         self._setupState = State(initialValue: state)
     }
 
+    /// Create a new AccountSetup view.
+    /// - Parameters:
+    ///   - setupComplete: The closure that is called once the account setup is considered to be completed.
+    ///     Note that it may be the case, that there are global account details associated (see ``Account/details``)
+    ///     but setup is not completed (e.g., after a login where additional info was required from the user).
+    ///   - header: An optional Header view to be displayed.
+    ///   - 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 },
+        @ViewBuilder header: () -> Header = { DefaultAccountSetupHeader() },
         @ViewBuilder `continue`: () -> Continue = { EmptyView() }
-    ) where Header == DefaultAccountSetupHeader {
-        self.init(continue: `continue`, header: { DefaultAccountSetupHeader() })
-    }
-
-
-    public init(
-        @ViewBuilder `continue`: () -> Continue = { EmptyView() },
-        @ViewBuilder header: () -> Header
     ) {
+        self.setupCompleteClosure = setupComplete
         self.header = header()
         self.continueButton = `continue`()
     }
+
+
+    @ViewBuilder
+    private func followUpInformationSheet(_ details: AccountDetails, requiredKeys: [any AccountKey.Type]) -> some View {
+        ProgressView()
+            .sheet(isPresented: $followUpSheet) {
+                NavigationStack {
+                    FollowUpInfoSheet(details: details, requiredKeys: requiredKeys)
+                }
+            }
+            .onAppear {
+                followUpSheet = true // we want full control through the setupState property
+            }
+            .onChange(of: followUpSheet) { newValue in
+                if !newValue { // follow up information was completed!
+                    setupState = .loadingExistingAccount
+                    setupCompleteClosure(details)
+                }
+            }
+    }
 }
 
 
@@ -203,15 +245,15 @@ struct AccountView_Previews: PreviewProvider {
         AccountSetup(state: .setupShown)
             .environmentObject(Account(building: detailsBuilder, active: MockUserIdPasswordAccountService()))
 
-        AccountSetup {
+        AccountSetup(continue: {
             Button(action: {
                 print("Continue")
             }, label: {
                 Text("Continue")
                     .frame(maxWidth: .infinity, minHeight: 38)
             })
-                .buttonStyle(.borderedProminent)
-        }
+            .buttonStyle(.borderedProminent)
+        })
             .environmentObject(Account(building: detailsBuilder, active: MockUserIdPasswordAccountService()))
     }
 }
diff --git a/Sources/SpeziAccount/AccountStorageStandard.swift b/Sources/SpeziAccount/AccountStorageStandard.swift
index ed653488..2fb4564b 100644
--- a/Sources/SpeziAccount/AccountStorageStandard.swift
+++ b/Sources/SpeziAccount/AccountStorageStandard.swift
@@ -16,7 +16,7 @@ import Spezi
 /// by your ``AccountService``, you may add an implementation of the `AccountStorageStandard` protocol to your App's `Standard`,
 /// inorder to handle storage and retrieval of these additional account values.
 ///
-/// - Note: You can use the ``AccountReference`` property wrapper to get access to the global ``Account`` object if you need it to implement additional functionality.
+/// - Note: You can use the ``Spezi/Standard/AccountReference`` property wrapper to get access to the global ``Account`` object if you need it to implement additional functionality.
 public protocol AccountStorageStandard: Standard {
     /// Create new associated account data.
     ///
@@ -49,10 +49,6 @@ public protocol AccountStorageStandard: Standard {
     ///
     /// This call is used to apply all modifications of the Standard-managed account values.
     ///
-    /// - Important: The ``ModifiedAccountDetails`` the ``AccountModifications`` structure might
-    ///     contain a change to the ``UserIdKey`` as well. This changes the primary ``AdditionalRecordId`` identifier
-    ///     used in all calls to reference a certain record. You must update the primary identifier!
-    ///
     /// - Note: A call to this method might certainly be immediately followed by a call to ``load(_:_:)``.
     ///
     /// - Parameters:
diff --git a/Sources/SpeziAccount/AccountValue/AccountKey+Views.swift b/Sources/SpeziAccount/AccountValue/AccountKey+Views.swift
index 720b6d1b..a475060e 100644
--- a/Sources/SpeziAccount/AccountValue/AccountKey+Views.swift
+++ b/Sources/SpeziAccount/AccountValue/AccountKey+Views.swift
@@ -26,14 +26,11 @@ extension AccountKey {
         AnyView(GeneralizedDataEntryView<DataEntry, Values>(initialValue: initialValue.value))
     }
 
-    static func dataEntryViewWithStoredValue<Values: AccountValues>(
+    static func dataEntryViewWithStoredValueOrInitial<Values: AccountValues>(
         details: AccountDetails,
         for values: Values.Type
-    ) -> AnyView? {
-        guard let value = details.storage.get(Self.self) else {
-            return nil
-        }
-
+    ) -> AnyView {
+        let value = details.storage.get(Self.self) ?? initialValue.value
         return AnyView(GeneralizedDataEntryView<DataEntry, Values>(initialValue: value))
     }
 
@@ -55,4 +52,8 @@ extension AccountKey {
 
         return AnyView(DataDisplay(value))
     }
+
+    static func singleEditView(model: AccountOverviewFormViewModel, details accountDetails: AccountDetails) -> AnyView {
+        AnyView(SingleEditView<Self>(model: model, details: accountDetails))
+    }
 }
diff --git a/Sources/SpeziAccount/AccountValue/AccountKey.swift b/Sources/SpeziAccount/AccountValue/AccountKey.swift
index 7a4cd171..95f8aa06 100644
--- a/Sources/SpeziAccount/AccountValue/AccountKey.swift
+++ b/Sources/SpeziAccount/AccountValue/AccountKey.swift
@@ -28,6 +28,7 @@ import XCTRuntimeAssertions
 /// ## Topics
 ///
 /// ### Builtin Account Keys
+/// - ``AccountIdKey``
 /// - ``UserIdKey``
 /// - ``PasswordKey``
 /// - ``PersonNameKey``
@@ -88,6 +89,12 @@ extension AccountKey {
     static var isRequired: Bool {
         self is any RequiredAccountKey.Type
     }
+
+    /// A ``AccountKeyCategory/credentials`` key that is not meant to be modified in
+    /// the `SecurityOverview` section in the ``AccountOverview``.
+    static var isHiddenCredential: Bool {
+        self == AccountIdKey.self || self == UserIdKey.self
+    }
 }
 
 
diff --git a/Sources/SpeziAccount/AccountValue/AccountKeyCollection.swift b/Sources/SpeziAccount/AccountValue/AccountKeyCollection.swift
index 25fbc1b9..fd496e48 100644
--- a/Sources/SpeziAccount/AccountValue/AccountKeyCollection.swift
+++ b/Sources/SpeziAccount/AccountValue/AccountKeyCollection.swift
@@ -69,6 +69,10 @@ public struct AccountKeyCollection: Sendable, AcceptingAccountKeyVisitor {
             .map { $0.key }
             .acceptAll(&visitor)
     }
+
+    public func contains<Key: AccountKey>(_ key: Key.Type) -> Bool {
+        elements.contains(where: { $0.key == key })
+    }
 }
 
 
diff --git a/Sources/SpeziAccount/AccountValue/Collections/AccountDetails.swift b/Sources/SpeziAccount/AccountValue/Collections/AccountDetails.swift
index 1262d749..fc55de04 100644
--- a/Sources/SpeziAccount/AccountValue/Collections/AccountDetails.swift
+++ b/Sources/SpeziAccount/AccountValue/Collections/AccountDetails.swift
@@ -36,6 +36,10 @@ public struct AccountDetails: Sendable, AccountValues {
     mutating func patchAccountService(_ service: any AccountService) {
         storage[ActiveAccountServiceKey.self] = service
     }
+
+    mutating func patchIsNewUser(_ isNewUser: Bool) {
+        storage[IsNewUserKey.self] = isNewUser
+    }
 }
 
 
diff --git a/Sources/SpeziAccount/AccountValue/Collections/ModifiedAccountDetails.swift b/Sources/SpeziAccount/AccountValue/Collections/ModifiedAccountDetails.swift
index f2de3db1..b1c7104c 100644
--- a/Sources/SpeziAccount/AccountValue/Collections/ModifiedAccountDetails.swift
+++ b/Sources/SpeziAccount/AccountValue/Collections/ModifiedAccountDetails.swift
@@ -19,3 +19,16 @@ public struct ModifiedAccountDetails: Sendable, AccountValues {
         self.storage = storage
     }
 }
+
+
+extension AccountValuesBuilder where Values == ModifiedAccountDetails {
+    func build(validation: Bool) throws -> Values {
+        let details = self.build()
+
+        if details.contains(AccountIdKey.self) {
+            throw AccountOperationError.accountIdChanged
+        }
+
+        return details
+    }
+}
diff --git a/Sources/SpeziAccount/AccountValue/Collections/SignupDetails.swift b/Sources/SpeziAccount/AccountValue/Collections/SignupDetails.swift
index 59540550..4d5f0b27 100644
--- a/Sources/SpeziAccount/AccountValue/Collections/SignupDetails.swift
+++ b/Sources/SpeziAccount/AccountValue/Collections/SignupDetails.swift
@@ -30,7 +30,7 @@ public struct SignupDetails: Sendable, AccountValues {
             let keyNames = missing.map { $0.keyPathDescription }
 
             LoggerKey.defaultValue.warning("\(keyNames) was/were required to be provided but wasn't/weren't provided!")
-            throw AccountValueConfigurationError.missingAccountValue(keyNames)
+            throw AccountOperationError.missingAccountValue(keyNames)
         }
     }
 }
@@ -40,7 +40,7 @@ extension AccountValuesBuilder where Values == SignupDetails {
     /// Building new ``SignupDetails`` while checking it's contents against the user-defined ``AccountValueConfiguration``.
     /// - Parameter configuration: The configured provided by the user (see ``Account/configuration``).
     /// - Returns: The built ``SignupDetails``.
-    /// - Throws: Throws potential ``AccountValueConfigurationError`` if requirements are not fulfilled.
+    /// - Throws: Throws potential ``AccountOperationError`` if requirements are not fulfilled.
     public func build(
         checking configuration: AccountValueConfiguration
     ) throws -> Values {
diff --git a/Sources/SpeziAccount/AccountValue/Configuration/AccountValueConfigurationError.swift b/Sources/SpeziAccount/AccountValue/Configuration/AccountOperationError.swift
similarity index 67%
rename from Sources/SpeziAccount/AccountValue/Configuration/AccountValueConfigurationError.swift
rename to Sources/SpeziAccount/AccountValue/Configuration/AccountOperationError.swift
index 2cac4e63..f328a0a9 100644
--- a/Sources/SpeziAccount/AccountValue/Configuration/AccountValueConfigurationError.swift
+++ b/Sources/SpeziAccount/AccountValue/Configuration/AccountOperationError.swift
@@ -9,14 +9,16 @@
 import Foundation
 
 
-/// An error that occurs due to restrictions or requirements of a ``AccountValueConfiguration``.
-public enum AccountValueConfigurationError: LocalizedError {
+/// An error that occurs due to restrictions or requirements (e.g., imposed by ``AccountValueConfiguration``).
+public enum AccountOperationError: LocalizedError {
     /// A ``AccountKeyRequirement/required`` ``AccountKey`` that was not supplied by the signup view before
     /// being passed to the ``AccountService``.
     ///
     /// - Note: This is an error in the view logic due to missing user-input sanitization or simply the view
     /// forgot to supply the ``AccountKey`` when building the ``SignupDetails``.
     case missingAccountValue(_ keyNames: [String])
+    /// The stable ``AccountIdKey`` was tried to be modified.
+    case accountIdChanged
 
 
     public var errorDescription: String? {
@@ -35,21 +37,22 @@ public enum AccountValueConfigurationError: LocalizedError {
     private var errorDescriptionValue: String.LocalizationValue {
         switch self {
         case .missingAccountValue:
-            return "ACCOUNT_VALUES_MISSING_VALUE_DESCRIPTION"
+            return "ACCOUNT_ERROR_VALUES_MISSING_VALUE_DESCRIPTION"
+        case .accountIdChanged:
+            return "ACCOUNT_ERROR_ACCOUNT_ID_CHANGED_DESCRIPTION"
         }
     }
 
     private var failureReasonValue: String.LocalizationValue {
         switch self {
         case let .missingAccountValue(keyName):
-            return "ACCOUNT_VALUES_MISSING_VALUE_REASON \(keyName.joined(separator: ", "))"
+            return "ACCOUNT_ERROR_VALUES_MISSING_VALUE_REASON \(keyName.joined(separator: ", "))"
+        case .accountIdChanged:
+            return "ACCOUNT_ERROR_ACCOUNT_ID_CHANGED_REASON"
         }
     }
 
     private var recoverySuggestionValue: String.LocalizationValue {
-        switch self {
-        case .missingAccountValue:
-            return "ACCOUNT_VALUES_MISSING_VALUE_RECOVERY"
-        }
+        "ACCOUNT_ERROR_RECOVERY"
     }
 }
diff --git a/Sources/SpeziAccount/AccountValue/Configuration/AccountValueConfiguration.swift b/Sources/SpeziAccount/AccountValue/Configuration/AccountValueConfiguration.swift
index b4a3961b..6d18454c 100644
--- a/Sources/SpeziAccount/AccountValue/Configuration/AccountValueConfiguration.swift
+++ b/Sources/SpeziAccount/AccountValue/Configuration/AccountValueConfiguration.swift
@@ -32,6 +32,49 @@ public struct AccountValueConfiguration {
     }
 
 
+    func all(filteredBy filter: [AccountKeyRequirement]? = nil) -> [any AccountKey.Type] {
+        // swiftlint:disable:previous discouraged_optional_collection
+
+        if let filter {
+            return self
+                .filter { configuration in
+                    filter.contains(configuration.requirement)
+                }
+                .map { $0.key }
+        } else {
+            return configuration.values.map { $0.key }
+        }
+    }
+
+    func allCategorized(filteredBy filter: [AccountKeyRequirement]? = nil) -> OrderedDictionary<AccountKeyCategory, [any AccountKey.Type]> {
+        // swiftlint:disable:previous discouraged_optional_collection
+        if let filter {
+            return self.reduce(into: [:]) { result, configuration in
+                guard filter.contains(configuration.requirement) else {
+                    return
+                }
+
+                result[configuration.key.category, default: []] += [configuration.key]
+            }
+        } else {
+            return self.reduce(into: [:]) { result, configuration in
+                result[configuration.key.category, default: []] += [configuration.key]
+            }
+        }
+    }
+
+    func missingRequiredKeys(for details: AccountDetails, includeCollected: Bool = false) -> [any AccountKey.Type] {
+        let accountKeyIds = Set(details.keys.map { ObjectIdentifier($0) })
+
+        return self
+            .all(filteredBy: includeCollected ? [.required, .collected] : [.required])
+            .filter { $0.category != .credentials } // don't collect credentials!
+            .filter { key in
+                !accountKeyIds.contains(ObjectIdentifier(key))
+            }
+    }
+
+
     /// Retrieve the configuration for a given type-erased ``AccountKey``.
     /// - Parameter key: The account key to query.
     /// - Returns: The configuration for a given ``AccountKey`` if it exists.
diff --git a/Sources/SpeziAccount/AccountValue/Keys/AccountIdKey.swift b/Sources/SpeziAccount/AccountValue/Keys/AccountIdKey.swift
new file mode 100644
index 00000000..5acd989d
--- /dev/null
+++ b/Sources/SpeziAccount/AccountValue/Keys/AccountIdKey.swift
@@ -0,0 +1,73 @@
+//
+// This source file is part of the Stanford Spezi open-source project
+//
+// SPDX-FileCopyrightText: 2023 Stanford University and the project authors (see CONTRIBUTORS.md)
+//
+// SPDX-License-Identifier: MIT
+//
+
+import SwiftUI
+
+
+/// The primary, unique, stable and typically internal identifier for an user account.
+///
+/// The `accountId` is used to uniquely identify a given account at any point in time.
+/// While the ``UserIdKey`` is typically the primary user-facing identifier and might change, the `accountId` is internal
+/// and a stable account identifier (e.g., to associate stored data to the account).
+///
+/// - Note: You should aim to use a string-based identifier, that doesn't contain any special characters to allow
+///     for maximum compatibility with other components.
+///
+/// ### Configuration
+/// As a user you don't need to worry about manually configuring the `accountId`. As it is not user-facing,
+/// you don't have to do anything.
+///
+/// As an ``AccountService`` you are required to supply the `accountId` for every ``AccountDetails`` you provide
+/// to ``Account/supplyUserDetails(_:isNewUser:)``. Further, if you supply a ``SupportedAccountKeys/exactly(_:)``
+/// configuration as part of your ``AccountServiceConfiguration``, make sure to include the `accountId` there as well.
+public struct AccountIdKey: RequiredAccountKey {
+    public typealias Value = String
+
+    public static let name = LocalizedStringResource("ACCOUNT_ID", bundle: .atURL(from: .module))
+
+    public static let category: AccountKeyCategory = .credentials
+}
+
+
+extension AccountKeys {
+    /// The accountId ``AccountIdKey``
+    public var accountId: AccountIdKey.Type {
+        AccountIdKey.self
+    }
+}
+
+
+extension AccountValues {
+    /// Access the account id of a user (see ``AccountIdKey``).
+    public var accountId: String {
+        storage[AccountIdKey.self]
+    }
+}
+
+
+extension AccountIdKey {
+    public struct DataDisplay: DataDisplayView {
+        public typealias Key = AccountIdKey
+
+        public var body: some View {
+            Text("The internal account identifier is not meant to be user facing!")
+        }
+
+        public init(_ value: Value) {}
+    }
+
+    public struct DataEntry: DataEntryView {
+        public typealias Key = AccountIdKey
+
+        public var body: some View {
+            Text("The internal account identifier is meant to be generated!")
+        }
+
+        public init(_ value: Binding<Key.Value>) {}
+    }
+}
diff --git a/Sources/SpeziAccount/AccountValue/Keys/IsNewUserKey.swift b/Sources/SpeziAccount/AccountValue/Keys/IsNewUserKey.swift
new file mode 100644
index 00000000..f3f87e8a
--- /dev/null
+++ b/Sources/SpeziAccount/AccountValue/Keys/IsNewUserKey.swift
@@ -0,0 +1,21 @@
+//
+// This source file is part of the Stanford Spezi open-source project
+//
+// SPDX-FileCopyrightText: 2023 Stanford University and the project authors (see CONTRIBUTORS.md)
+//
+// SPDX-License-Identifier: MIT
+//
+
+import Spezi
+
+
+struct IsNewUserKey: KnowledgeSource {
+    typealias Anchor = AccountAnchor
+    typealias Value = Bool
+}
+
+extension AccountDetails {
+    var isNewUser: Bool {
+        storage[IsNewUserKey.self] ?? false
+    }
+}
diff --git a/Sources/SpeziAccount/AccountValue/Keys/PasswordKey.swift b/Sources/SpeziAccount/AccountValue/Keys/PasswordKey.swift
index a077da35..3a413f7c 100644
--- a/Sources/SpeziAccount/AccountValue/Keys/PasswordKey.swift
+++ b/Sources/SpeziAccount/AccountValue/Keys/PasswordKey.swift
@@ -21,7 +21,7 @@ import SwiftUI
 /// ### Password UI
 ///
 /// - ``PasswordFieldType``
-public struct PasswordKey: RequiredAccountKey {
+public struct PasswordKey: AccountKey {
     public typealias Value = String
 
     public static let name = LocalizedStringResource("UP_PASSWORD", bundle: .atURL(from: .module))
@@ -42,7 +42,7 @@ extension AccountKeys {
 
 extension SignupDetails {
     /// Access the password of a user in the ``SignupDetails``.
-    public var password: String {
+    public var password: String? {
         storage[PasswordKey.self]
     }
 }
diff --git a/Sources/SpeziAccount/AccountValue/Keys/UserIdKey.swift b/Sources/SpeziAccount/AccountValue/Keys/UserIdKey.swift
index c838721c..99ceb3d5 100644
--- a/Sources/SpeziAccount/AccountValue/Keys/UserIdKey.swift
+++ b/Sources/SpeziAccount/AccountValue/Keys/UserIdKey.swift
@@ -6,23 +6,38 @@
 // SPDX-License-Identifier: MIT
 //
 
+import Spezi
 import SwiftUI
 
 
-/// A string-based, unique user identifier.
+/// A string-based, user-facing, unique user identifier.
 ///
-/// The `userId` is used to uniquely identify a given account. The value might carry
-/// additional semantics. For example, the `userId` might, at the same time, be the primary email address
-/// of the user. Such semantics can be controlled by the ``AccountService``
+/// The `userId` is used to uniquely identify a given account at a given point in time.
+/// While the ``AccountIdKey`` is guaranteed to be stable, the `userId` might change over time.
+/// But it will still be unique.
+///
+/// - Note: If an ``AccountService`` doesn't provide a `userId`, it will fallback to return the ``AccountIdKey``.
+///
+/// The value might carry additional semantics. For example, the `userId` might, at the same time,
+/// be the primary email address of the user. Such semantics can be controlled by the ``AccountService``
 /// using the ``UserIdType`` configuration.
 ///
 /// - Note: You may also refer to the ``EmailAddressKey`` to query the email address of an account.
-public struct UserIdKey: RequiredAccountKey {
+public struct UserIdKey: AccountKey, ComputedKnowledgeSource {
+    public typealias StoragePolicy = AlwaysCompute
     public typealias Value = String
 
     public static let name = LocalizedStringResource("USER_ID", bundle: .atURL(from: .module))
 
     public static let category: AccountKeyCategory = .credentials
+
+    public static func compute<Repository: SharedRepository<AccountAnchor>>(from repository: Repository) -> String {
+        if let value = repository.get(Self.self) {
+            return value // return the userId if there is one stored
+        }
+
+        return repository[AccountIdKey.self] // otherwise return the primary account key
+    }
 }
 
 
diff --git a/Sources/SpeziAccount/Environment/AccountRequiredKey.swift b/Sources/SpeziAccount/Environment/AccountRequiredKey.swift
new file mode 100644
index 00000000..4ed653db
--- /dev/null
+++ b/Sources/SpeziAccount/Environment/AccountRequiredKey.swift
@@ -0,0 +1,29 @@
+//
+// This source file is part of the Stanford Spezi Template Application project
+//
+// SPDX-FileCopyrightText: 2023 Stanford University
+//
+// SPDX-License-Identifier: MIT
+//
+
+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 ``SwiftUI/View/accountRequired(_:setupSheet:)``.
+    public var accountRequired: Bool {
+        get {
+            self[AccountRequiredKey.self]
+        }
+        set {
+            self[AccountRequiredKey.self] = newValue
+        }
+    }
+}
diff --git a/Sources/SpeziAccount/Mock/MockUserIdPasswordAccountService.swift b/Sources/SpeziAccount/Mock/MockUserIdPasswordAccountService.swift
index bc25cdbd..1d74d21c 100644
--- a/Sources/SpeziAccount/Mock/MockUserIdPasswordAccountService.swift
+++ b/Sources/SpeziAccount/Mock/MockUserIdPasswordAccountService.swift
@@ -14,6 +14,7 @@ public actor MockUserIdPasswordAccountService: UserIdPasswordAccountService {
     @AccountReference private var account: Account
 
     public let configuration: AccountServiceConfiguration
+    private var userIdToAccountId: [String: UUID] = [:]
 
 
     /// Create a new userId- and password-based account service.
@@ -34,6 +35,7 @@ public actor MockUserIdPasswordAccountService: UserIdPasswordAccountService {
         try await Task.sleep(for: .seconds(1))
 
         let details = AccountDetails.Builder()
+            .set(\.accountId, value: userIdToAccountId[userId, default: UUID()].uuidString)
             .set(\.userId, value: userId)
             .set(\.name, value: PersonNameComponents(givenName: "Andreas", familyName: "Bauer"))
             .build(owner: self)
@@ -44,7 +46,11 @@ public actor MockUserIdPasswordAccountService: UserIdPasswordAccountService {
         print("Mock Signup: \(signupDetails)")
         try await Task.sleep(for: .seconds(1))
 
+        let id = UUID()
+        userIdToAccountId[signupDetails.userId] = id
+
         let details = AccountDetails.Builder(from: signupDetails)
+            .set(\.accountId, value: id.uuidString)
             .remove(\.password)
             .build(owner: self)
         try await account.supplyUserDetails(details)
diff --git a/Sources/SpeziAccount/Model/AdditionalRecordId.swift b/Sources/SpeziAccount/Model/AdditionalRecordId.swift
index 61274489..e5f00e93 100644
--- a/Sources/SpeziAccount/Model/AdditionalRecordId.swift
+++ b/Sources/SpeziAccount/Model/AdditionalRecordId.swift
@@ -9,18 +9,18 @@
 
 /// A stable identifier used by ``AccountStorageStandard`` instances to identity a set of additionally stored records.
 ///
-/// The identifier is built by combining a stable ``AccountService`` identifier and the primary userId (see ``UserIdKey``).
+/// The identifier is built by combining a stable ``AccountService`` identifier and the primary accountID (see ``AccountIdKey``).
 /// Using both, additional data records of a user can be uniquely identified across ``AccountService`` implementations.
 public struct AdditionalRecordId: CustomStringConvertible, Hashable, Identifiable {
     /// A stable ``AccountService`` identifier. See ``AccountService/id-83c6c``.
     public let accountServiceId: String
-    /// The primary user identifier. See ``UserIdKey``.
-    public let userId: String
+    /// The primary user identifier. See ``AccountIdKey``.
+    public let accountId: String
 
 
     /// String representation of the record identifier.
     public var description: String {
-        accountServiceId + "-" + userId
+        accountServiceId + "-" + accountId
     }
 
     /// The identifier.
@@ -29,9 +29,9 @@ public struct AdditionalRecordId: CustomStringConvertible, Hashable, Identifiabl
     }
 
 
-    init(serviceId accountServiceId: String, userId: String) {
+    init(serviceId accountServiceId: String, accountId: String) {
         self.accountServiceId = accountServiceId
-        self.userId = userId
+        self.accountId = accountId
     }
 
 
@@ -42,6 +42,6 @@ public struct AdditionalRecordId: CustomStringConvertible, Hashable, Identifiabl
 
     public func hash(into hasher: inout Hasher) {
         accountServiceId.hash(into: &hasher)
-        userId.hash(into: &hasher)
+        accountId.hash(into: &hasher)
     }
 }
diff --git a/Sources/SpeziAccount/Model/Validation/ValidationEngines.swift b/Sources/SpeziAccount/Model/Validation/ValidationEngines.swift
index ac51c5f3..8d973c46 100644
--- a/Sources/SpeziAccount/Model/Validation/ValidationEngines.swift
+++ b/Sources/SpeziAccount/Model/Validation/ValidationEngines.swift
@@ -171,6 +171,8 @@ public class ValidationEngines<FieldIdentifier: Hashable>: ObservableObject {
     // deliberately not @Published, registered methods should not trigger an UI update
     private var storage: OrderedDictionary<UUID, RegisteredEngine<FieldIdentifier>>
 
+    private var hooks: [String: () -> Void] = [:]
+
     /// Reports input validity of all registered ``ValidationEngine``s.
     @MainActor public var allInputValid: Bool {
         storage.values
@@ -230,9 +232,21 @@ public class ValidationEngines<FieldIdentifier: Hashable>: ObservableObject {
         }
     }
 
+    func register(id: String, hook: @escaping () -> Void) {
+        hooks[id] = hook
+    }
+
+    func remove(hook: String) {
+        hooks[hook] = nil
+    }
+
     @MainActor
     private func collectFailedResults() -> [FailedResult<FieldIdentifier>] {
-        storage.values.compactMap { engine in
+        for hook in hooks.values {
+            hook()
+        }
+
+        return storage.values.compactMap { engine in
             engine()
         }
     }
diff --git a/Sources/SpeziAccount/Resources/de.lproj/Localizable.strings b/Sources/SpeziAccount/Resources/de.lproj/Localizable.strings
index a107688a..16cd9fb1 100644
--- a/Sources/SpeziAccount/Resources/de.lproj/Localizable.strings
+++ b/Sources/SpeziAccount/Resources/de.lproj/Localizable.strings
@@ -20,9 +20,11 @@
 "ACCOUNT_WELCOME_SIGNED_IN_SUBTITLE" = "Du bist bereits mit dem folgenden Benutzerkonto angemeldet. Du kannst dein Benutzerkonto ändern, indem du dich abmeldest.";
 
 // MARK: - AccountValues
-"ACCOUNT_VALUES_MISSING_VALUE_DESCRIPTION" = "Fehlende Werte im Benutzerkonto";
-"ACCOUNT_VALUES_MISSING_VALUE_REASON %@" = "Die folgenden Werte wurden nicht im Benutzerkonto gesetzt: %@";
-"ACCOUNT_VALUES_MISSING_VALUE_RECOVERY" = "Melde dieses Problem dem Entwickler des Account Services.";
+"ACCOUNT_ERROR_VALUES_MISSING_VALUE_DESCRIPTION" = "Fehlende Werte im Benutzerkonto";
+"ACCOUNT_ERROR_VALUES_MISSING_VALUE_REASON %@" = "Die folgenden Werte wurden nicht im Benutzerkonto gesetzt: %@.";
+"ACCOUNT_ERROR_ACCOUNT_ID_CHANGED_DESCRIPTION" = "Anfrage Fehlgeschlagen";
+"ACCOUNT_ERROR_ACCOUNT_ID_CHANGED_REASON" = "Deine primäre Benutzerkennung kann nicht geändert werden da diese dauerhaft vergeben wird!";
+"ACCOUNT_ERROR_RECOVERY" = "Melde dieses Problem dem Entwickler des Account Services.";
 
 // MARK: - UserId and Password
 "UP_PASSWORD" = "Passwort";
@@ -40,7 +42,8 @@
 "UP_LOGIN_FAILED_DEFAULT_ERROR" = "Anmeldung fehlgeschlagen!";
 
 // MARK: - UserId and Password (Signup)
-"UP_SIGNUP_INSTRUCTIONS" = "Please fill out the details below to create a new account.";
+"UP_SIGNUP_HEADER" = "Erstelle dein Benutzerkonto";
+"UP_SIGNUP_INSTRUCTIONS" = "Bitte fülle die folgenden Informationen aus, um ein neues Benutzerkonto zu erstellen.";
 "UP_CREDENTIALS" = "Zugangsdaten";
 "UP_NAME" = "Name";
 "UP_CONTACT_DETAILS" = "Kontaktdaten";
@@ -54,6 +57,11 @@
 "UAP_RESET_PASSWORD_PROCESS_SUCCESSFUL_LABEL" = "Ein Link zum Zurücksetzen das Passworts wurde versandt.";
 "UAP_RESET_PASSWORD_FAILED_DEFAULT_ERROR" = "Passwort Zurücksetzen fehlgeschlagen!";
 
+// MARK: - Follow-Up Information
+"FOLLOW_UP_INFORMATION_TITLE" = "Account Vervollständigen";
+"FOLLOW_UP_INFORMATION_INSTRUCTIONS" = "Bitte fülle die folgenden Informationen aus, um dein Benutzerkonto zu vervollständigen.";
+"FOLLOW_UP_INFORMATION_COMPLETE" = "Fertig";
+
 // MARK: - Account Summary
 "UP_LOGOUT_FAILED_DEFAULT_ERROR" = "Abmelden fehlgeschlagen!";
 
@@ -66,7 +74,7 @@
 "CANCEL" = "Abbrechen";
 "ACCOUNT_OVERVIEW_EDIT_DEFAULT_ERROR" = "Speichern deiner Änderung fehlgeschlagen!";
 "REMOVE_DEFAULT_ERROR"= "Löschen fehlgeschlagen!";
-"SECURITY" = "Sicherheit";
+"SIGN_IN_AND_SECURITY" = "Anmeldung & Sicherheit";
 "VALUE_ADD %@" = "%@ Hinzufügen";
 "CHANGE_PASSWORD" = "Passwort Ändern";
 
@@ -75,9 +83,11 @@
 
 // MARK - Confirmation Dialogs
 "CONFIRMATION_DISCARD_CHANGES_TITLE" = "Willst du alle Änderungen verwerfen?";
-"CONFIRMATION_DISCARD_INPUT_TITLE" = "Willst du deine Eingaben verwerfen?";
 "CONFIRMATION_DISCARD_CHANGES" = "Änderungen Verwerfen";
+"CONFIRMATION_DISCARD_INPUT_TITLE" = "Willst du deine Eingaben verwerfen?";
 "CONFIRMATION_DISCARD_INPUT" = "Eingaben Verwerfen";
+"CONFIRMATION_DISCARD_ADDITIONAL_INFO_TITLE" = "Diese Kontoinformationen sind erfoderlich. Wenn du abbrichst, wirst du automatisch abgemeldet!";
+"CONFIRMATION_DISCARD_ADDITIONAL_INFO" = "Abbrechen und Abmelden";
 "CONFIRMATION_KEEP_EDITING"= "Weiter Bearbeiten";
 "CONFIRMATION_LOGOUT" = "Willst du dich wirklich abmelden?";
 "CONFIRMATION_REMOVAL" = "Willst du wirklich dein Benutzerkonto löschen?";
@@ -93,15 +103,14 @@
 "VALIDATION_RULE_GIVEN_NAME_EMPTY" = "Dein Vorname kann nicht leer sein!";
 "VALIDATION_RULE_FAMILY_NAME_EMPTY" = "Dein Nachname kann nicht leer sein!";
 
+// MARK: - Account Id
+"ACCOUNT_ID" = "Benutzerkennung";
+
 // MARK: - User Id
-"USER_ID" = "Benutzerkennung";
+"USER_ID" = "Benutzername";
 "USER_ID_EMAIL" = "E-Mail Adresse";
 "USER_ID_USERNAME" = "Benutzername";
 
-// MARK: - General Views
-"LOGIN" = "Anmelden";
-"SIGN_UP" = "Benutzerkonto Erstellen";
-
 // MARK: - Person Name
 "NAME" = "Name";
 "UAP_SIGNUP_GIVEN_NAME_TITLE" = "Vorname";
diff --git a/Sources/SpeziAccount/Resources/en.lproj/Localizable.strings b/Sources/SpeziAccount/Resources/en.lproj/Localizable.strings
index 161d19f8..336894c3 100644
--- a/Sources/SpeziAccount/Resources/en.lproj/Localizable.strings
+++ b/Sources/SpeziAccount/Resources/en.lproj/Localizable.strings
@@ -19,10 +19,12 @@
 "ACCOUNT_WELCOME_SUBTITLE" = "Please login to your account. Or create a new one if you don't have one already.";
 "ACCOUNT_WELCOME_SIGNED_IN_SUBTITLE" = "You are already logged in with the account shown below. Continue or change your account by logging out.";
 
-// MARK: - AccountValues
-"ACCOUNT_VALUES_MISSING_VALUE_DESCRIPTION" = "Missing Account Values";
-"ACCOUNT_VALUES_MISSING_VALUE_REASON %@" = "The following required account values were not supplied: %@";
-"ACCOUNT_VALUES_MISSING_VALUE_RECOVERY" = "Raise an issue with the developer of the Account Service.";
+// MARK: - Account Operation Error
+"ACCOUNT_ERROR_VALUES_MISSING_VALUE_DESCRIPTION" = "Missing Account Values";
+"ACCOUNT_ERROR_VALUES_MISSING_VALUE_REASON %@" = "The following required account values were not supplied: %@.";
+"ACCOUNT_ERROR_ACCOUNT_ID_CHANGED_DESCRIPTION" = "Failed Account Operation";
+"ACCOUNT_ERROR_ACCOUNT_ID_CHANGED_REASON" = "You primary account identifier cannot be changed as it is required to be stable!";
+"ACCOUNT_ERROR_RECOVERY" = "Raise an issue with the developer of the Account Service.";
 
 // MARK: - UserId and Password
 "UP_PASSWORD" = "Password";
@@ -40,7 +42,8 @@
 "UP_LOGIN_FAILED_DEFAULT_ERROR" = "Could not login!";
 
 // MARK: - UserId and Password (Signup)
-"UP_SIGNUP_INSTRUCTIONS" = "Please fill out the details below to create a new account.";
+"UP_SIGNUP_HEADER" = "Create a new Account";
+"UP_SIGNUP_INSTRUCTIONS" = "Please fill out the details below to create your new account.";
 "UP_CREDENTIALS" = "Credentials";
 "UP_NAME" = "Name";
 "UP_CONTACT_DETAILS" = "Contact Details";
@@ -54,6 +57,11 @@
 "UAP_RESET_PASSWORD_PROCESS_SUCCESSFUL_LABEL" = "Sent out a link to reset the password.";
 "UAP_RESET_PASSWORD_FAILED_DEFAULT_ERROR" = "Could not reset the password!";
 
+// MARK: - Follow-Up Information
+"FOLLOW_UP_INFORMATION_TITLE" = "Finish Account Setup";
+"FOLLOW_UP_INFORMATION_INSTRUCTIONS" = "Please fill out the details below to complete your account setup.";
+"FOLLOW_UP_INFORMATION_COMPLETE" = "Complete";
+
 // MARK: - Account Summary
 "UP_LOGOUT_FAILED_DEFAULT_ERROR" = "Could not logout!";
 
@@ -66,7 +74,7 @@
 "CANCEL" = "Cancel";
 "ACCOUNT_OVERVIEW_EDIT_DEFAULT_ERROR" = "Could not save account details!";
 "REMOVE_DEFAULT_ERROR"= "Could not remove account!";
-"SECURITY" = "Security";
+"SIGN_IN_AND_SECURITY" = "Sign-In & Security";
 "VALUE_ADD %@" = "Add %@";
 "CHANGE_PASSWORD" = "Change Password";
 
@@ -75,9 +83,11 @@
 
 // MARK - Confirmation Dialogs
 "CONFIRMATION_DISCARD_CHANGES_TITLE" = "Are you sure you want to discard your changes?";
-"CONFIRMATION_DISCARD_INPUT_TITLE" = "Are you sure you want to discard your input?";
 "CONFIRMATION_DISCARD_CHANGES" = "Discard Changes";
+"CONFIRMATION_DISCARD_INPUT_TITLE" = "Are you sure you want to discard your input?";
 "CONFIRMATION_DISCARD_INPUT" = "Discard Input";
+"CONFIRMATION_DISCARD_ADDITIONAL_INFO_TITLE" = "This account information is required. If you abort, you will automatically be signed out!";
+"CONFIRMATION_DISCARD_ADDITIONAL_INFO" = "Cancel and Logout";
 "CONFIRMATION_KEEP_EDITING"= "Keep Editing";
 "CONFIRMATION_LOGOUT" = "Are you sure you want to logout?";
 "CONFIRMATION_REMOVAL" = "Are you sure you want to delete your account?";
@@ -93,6 +103,9 @@
 "VALIDATION_RULE_GIVEN_NAME_EMPTY" = "The first name field cannot be empty!";
 "VALIDATION_RULE_FAMILY_NAME_EMPTY" = "The last name field cannot be empty!";
 
+// MARK: - Account Id
+"ACCOUNT_ID" = "Account Identifier";
+
 // MARK: - User Id
 "USER_ID" = "User Identifier";
 "USER_ID_EMAIL" = "E-Mail Address";
diff --git a/Sources/SpeziAccount/SpeziAccount.docc/Account Services/Creating your own Account Service.md b/Sources/SpeziAccount/SpeziAccount.docc/Account Services/Creating your own Account Service.md
index 31542915..8fd9e09f 100644
--- a/Sources/SpeziAccount/SpeziAccount.docc/Account Services/Creating your own Account Service.md	
+++ b/Sources/SpeziAccount/SpeziAccount.docc/Account Services/Creating your own Account Service.md	
@@ -49,11 +49,11 @@ Apart from implementing the ``AccountService`` protocol, an account service is r
 of any changes of the user state (e.g., user information updated remotely).
 
 To do so, you can use the ``AccountService/AccountReference`` property wrapper to get access to the ``Account`` context.
-You can then use the ``Account/supplyUserDetails(_:)`` and ``Account/removeUserDetails()`` methods
+You can then use the ``Account/supplyUserDetails(_:isNewUser:)`` and ``Account/removeUserDetails()`` methods
 to update the account state.
 Below is a short code example that implements a basic remote session expiration handler.
 
-> Note: You will always need to call the ``Account/supplyUserDetails(_:)`` and ``Account/removeUserDetails()`` methods manually,
+> Note: You will always need to call the ``Account/supplyUserDetails(_:isNewUser:)`` and ``Account/removeUserDetails()`` methods manually,
 even if the change in user state is caused by a local operation like ``AccountService/signUp(signupDetails:)`` or ``AccountService/logout()``.
 
 ```swift
@@ -126,5 +126,5 @@ class MyComponent: Component {
 
 ### Managing Account Details
 
-- ``Account/supplyUserDetails(_:)``
+- ``Account/supplyUserDetails(_:isNewUser:)``
 - ``Account/removeUserDetails()``
diff --git a/Sources/SpeziAccount/SpeziAccount.docc/Setup Guides/Initial Setup.md b/Sources/SpeziAccount/SpeziAccount.docc/Setup Guides/Initial Setup.md
index a5eb67c5..6e3be2b9 100644
--- a/Sources/SpeziAccount/SpeziAccount.docc/Setup Guides/Initial Setup.md	
+++ b/Sources/SpeziAccount/SpeziAccount.docc/Setup Guides/Initial Setup.md	
@@ -92,7 +92,7 @@ struct MyView: View {
 }
 ```
 
-> Note: You can also customize the header text using the ``AccountSetup/init(continue:header:)`` initializer.
+> Note: You can also customize the header text using the ``AccountSetup/init(setupComplete:header:continue:)`` initializer.
 
 ### Account Overview
 
@@ -116,12 +116,13 @@ struct MyView: View {
 - ``AccountValueConfiguration``
 - ``AccountKeyRequirement``
 - ``AccountKeyConfiguration``
-- ``AccountValueConfigurationError``
+- ``AccountOperationError``
 
 ### Views
 
 - ``AccountSetup``
 - ``AccountOverview``
+- ``AccountHeader``
 
 ### Reacting to Events
 
diff --git a/Sources/SpeziAccount/ViewModel/AccountOverviewFormViewModel.swift b/Sources/SpeziAccount/ViewModel/AccountOverviewFormViewModel.swift
index 5e032c10..32f46af8 100644
--- a/Sources/SpeziAccount/ViewModel/AccountOverviewFormViewModel.swift
+++ b/Sources/SpeziAccount/ViewModel/AccountOverviewFormViewModel.swift
@@ -49,9 +49,7 @@ class AccountOverviewFormViewModel: ObservableObject {
 
 
     init(account: Account) {
-        self.categorizedAccountKeys = account.configuration.reduce(into: [:]) { result, configuration in
-            result[configuration.key.category, default: []] += [configuration.key]
-        }
+        self.categorizedAccountKeys = account.configuration.allCategorized()
 
         // We forward the objectWillChange publisher. Our `hasUnsavedChanges` is affected by changes to the builder.
         // Otherwise, changes to the object wouldn't be important.
@@ -65,13 +63,22 @@ class AccountOverviewFormViewModel: ObservableObject {
 
 
     func accountKeys(by category: AccountKeyCategory, using details: AccountDetails) -> [any AccountKey.Type] {
-        categorizedAccountKeys[category, default: []]
+        var result = categorizedAccountKeys[category, default: []]
             .sorted(using: AccountOverviewValuesComparator(details: details, added: addedAccountKeys, removed: removedAccountKeys))
+
+        for describedKey in details.accountService.configuration.requiredAccountKeys
+            where describedKey.key.category == category {
+            result.append(describedKey.key)
+        }
+
+        return result
     }
 
-    func editableAccountKeys(details accountDetails: AccountDetails) -> OrderedDictionary<AccountKeyCategory, [any AccountKey.Type]> {
-        let results = categorizedAccountKeys.filter { category, _ in
-            category != .credentials && category != .name
+    private func baseSortedAccountKeys(details accountDetails: AccountDetails) -> OrderedDictionary<AccountKeyCategory, [any AccountKey.Type]> {
+        var results = categorizedAccountKeys
+
+        for describedKey in accountDetails.accountService.configuration.requiredAccountKeys {
+            results[describedKey.key.category, default: []] += [describedKey.key]
         }
 
         // We want to establish the following order:
@@ -84,6 +91,27 @@ class AccountOverviewFormViewModel: ObservableObject {
         }
     }
 
+    func editableAccountKeys(details accountDetails: AccountDetails) -> OrderedDictionary<AccountKeyCategory, [any AccountKey.Type]> {
+        baseSortedAccountKeys(details: accountDetails).filter { category, _ in
+            category != .credentials && category != .name
+        }
+    }
+
+    func namesOverviewKeys(details accountDetails: AccountDetails) -> [any AccountKey.Type] {
+        var result = baseSortedAccountKeys(details: accountDetails)
+            .filter { category, _ in
+                category == .credentials || category == .name
+            }
+
+        if result[.credentials]?.contains(where: { $0 == UserIdKey.self }) == true {
+            result[.credentials] = [UserIdKey.self] // ensure userId is the only credential we display
+        }
+
+        return result.reduce(into: []) { result, tuple in
+            result.append(contentsOf: tuple.value)
+        }
+    }
+
     func addAccountDetail(for value: any AccountKey.Type) {
         guard !addedAccountKeys.contains(value) else {
             return
@@ -144,8 +172,8 @@ class AccountOverviewFormViewModel: ObservableObject {
         let removedDetailsBuilder = RemovedAccountDetails.Builder()
         removedDetailsBuilder.merging(with: removedAccountKeys.keys, from: details)
 
-        let modifications = AccountModifications(
-            modifiedDetails: modifiedDetailsBuilder.build(),
+        let modifications = try AccountModifications(
+            modifiedDetails: modifiedDetailsBuilder.build(validation: true),
             removedAccountDetails: removedDetailsBuilder.build()
         )
 
@@ -177,16 +205,14 @@ class AccountOverviewFormViewModel: ObservableObject {
         return userId
     }
 
-    func accountSecurityLabel(_ configuration: AccountValueConfiguration) -> Text {
-        let security = Text("SECURITY", bundle: .module)
-
-        if configuration[PasswordKey.self] != nil {
-            return Text("UP_PASSWORD", bundle: .module)
-                + Text(" & ")
-                + security
-        }
+    func displaysSignInSecurityDetails(_ details: AccountDetails) -> Bool {
+        accountKeys(by: .credentials, using: details)
+            .contains(where: { !$0.isHiddenCredential })
+    }
 
-        return security
+    func displaysNameDetails() -> Bool {
+        categorizedAccountKeys[.credentials]?.contains(where: { $0 == UserIdKey.self }) == true
+            || categorizedAccountKeys[.name]?.isEmpty != true
     }
 
 
diff --git a/Sources/SpeziAccount/ViewModifier/AccountRequiredModifier.swift b/Sources/SpeziAccount/ViewModifier/AccountRequiredModifier.swift
new file mode 100644
index 00000000..86c8fc03
--- /dev/null
+++ b/Sources/SpeziAccount/ViewModifier/AccountRequiredModifier.swift
@@ -0,0 +1,69 @@
+//
+// This source file is part of the Stanford Spezi Template Application project
+//
+// SPDX-FileCopyrightText: 2023 Stanford University
+//
+// SPDX-License-Identifier: MIT
+//
+
+import SwiftUI
+
+
+struct AccountRequiredModifier<SetupSheet: View>: ViewModifier {
+    private let required: Bool
+    private let setupSheet: SetupSheet
+
+    @EnvironmentObject private var account: Account
+
+    @State private var presentingSheet = false
+
+
+    init(required: Bool, @ViewBuilder setupSheet: () -> SetupSheet) {
+        self.required = required
+        self.setupSheet = setupSheet()
+    }
+
+
+    func body(content: Content) -> some View {
+        if required {
+            content
+                .onChange(of: [account.signedIn, presentingSheet]) { _ in
+                    if !account.signedIn && !presentingSheet {
+                        presentingSheet = true
+                    }
+                }
+                .task {
+                    try? await Task.sleep(for: .milliseconds(500))
+                    if !account.signedIn {
+                        presentingSheet = true
+                    }
+                }
+                .sheet(isPresented: $presentingSheet) {
+                    setupSheet
+                        .interactiveDismissDisabled(true)
+                }
+                .environment(\.accountRequired, true)
+        } else {
+            content
+        }
+    }
+}
+
+
+extension View {
+    /// 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 ``SwiftUI/EnvironmentValues
+    ///
+    /// - Parameters:
+    ///   - required: The flag indicating if an account is required at all times.
+    ///   - 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<SetupSheet: View>(_ required: Bool, @ViewBuilder setupSheet: () -> SetupSheet) -> some View {
+        modifier(AccountRequiredModifier(required: required, setupSheet: setupSheet))
+    }
+}
diff --git a/Sources/SpeziAccount/ViewModifier/VerifyRequiredAccountDetailsModifier.swift b/Sources/SpeziAccount/ViewModifier/VerifyRequiredAccountDetailsModifier.swift
new file mode 100644
index 00000000..dad206b9
--- /dev/null
+++ b/Sources/SpeziAccount/ViewModifier/VerifyRequiredAccountDetailsModifier.swift
@@ -0,0 +1,78 @@
+//
+// This source file is part of the Stanford Spezi open-source project
+//
+// SPDX-FileCopyrightText: 2023 Stanford University and the project authors (see CONTRIBUTORS.md)
+//
+// SPDX-License-Identifier: MIT
+//
+
+import SwiftUI
+
+
+private struct FollowUpSession: Identifiable {
+    var id: String {
+        details.userId
+    }
+
+    let details: AccountDetails
+    let requiredKeys: [any AccountKey.Type]
+}
+
+
+struct VerifyRequiredAccountDetailsModifier: ViewModifier {
+    private let verify: Bool
+
+    @EnvironmentObject private var account: Account
+
+    @SceneStorage("edu.stanford.spezi-account.startup-account-check") private var verifiedAccount = false
+    @State private var followUpSession: FollowUpSession?
+
+
+    init(verify: Bool) {
+        self.verify = verify
+    }
+
+
+    func body(content: Content) -> some View {
+        if verify {
+            content
+                .sheet(item: $followUpSession) { session in
+                    FollowUpInfoSheet(details: session.details, requiredKeys: session.requiredKeys)
+                }
+                .task {
+                    guard !verifiedAccount else {
+                        return
+                    }
+
+                    try? await Task.sleep(for: .milliseconds(500))
+                    verifiedAccount = true
+
+                    if let details = account.details {
+                        let missingKeys = account.configuration.missingRequiredKeys(for: details)
+
+                        if !missingKeys.isEmpty {
+                            followUpSession = FollowUpSession(details: details, requiredKeys: missingKeys)
+                        }
+                    }
+                }
+        } else {
+            content
+        }
+    }
+}
+
+
+extension View {
+    /// Used this modifier to ensure that all user accounts in your app are up to date with your
+    /// SpeziAccount configuration.
+    ///
+    /// Withing your ``AccountConfiguration`` you define your app-global ``AccountValueConfiguration`` that defines
+    /// what ``AccountKey`` are required and collected at signup. You can use this modifier to collect additional information
+    /// form existing users, should your configuration of **required** account keys change between one of your releases.
+    ///
+    /// - Parameter verify: Flag indicating if this verification check is turned on.
+    /// - Returns: The modified view.
+    public func verifyRequiredAccountDetails(_ verify: Bool = true) -> some View {
+        modifier(VerifyRequiredAccountDetailsModifier(verify: verify))
+    }
+}
diff --git a/Sources/SpeziAccount/Views/AccountOverview/AccountKeyEditRow.swift b/Sources/SpeziAccount/Views/AccountOverview/AccountKeyOverviewRow.swift
similarity index 92%
rename from Sources/SpeziAccount/Views/AccountOverview/AccountKeyEditRow.swift
rename to Sources/SpeziAccount/Views/AccountOverview/AccountKeyOverviewRow.swift
index 6bc04fff..08af9793 100644
--- a/Sources/SpeziAccount/Views/AccountOverview/AccountKeyEditRow.swift
+++ b/Sources/SpeziAccount/Views/AccountOverview/AccountKeyOverviewRow.swift
@@ -9,7 +9,7 @@
 import SwiftUI
 
 
-struct AccountKeyEditRow: View {
+struct AccountKeyOverviewRow: View {
     private let accountDetails: AccountDetails
     private let accountKey: any AccountKey.Type
 
@@ -26,8 +26,8 @@ struct AccountKeyEditRow: View {
                     Group {
                         if let view = accountKey.dataEntryViewFromBuilder(builder: model.modifiedDetailsBuilder, for: ModifiedAccountDetails.self) {
                             view
-                        } else if let view = accountKey.dataEntryViewWithStoredValue(details: accountDetails, for: ModifiedAccountDetails.self) {
-                            view
+                        } else {
+                            accountKey.dataEntryViewWithStoredValueOrInitial(details: accountDetails, for: ModifiedAccountDetails.self)
                         }
                     }
                         .environment(\.accountViewType, .overview(mode: .existing))
@@ -91,7 +91,7 @@ struct AccountKeyEditRow_Previews: PreviewProvider {
 
     static var previews: some View {
         if let details = account.details {
-            AccountKeyEditRow(details: details, for: GenderIdentityKey.self, model: model)
+            AccountKeyOverviewRow(details: details, for: GenderIdentityKey.self, model: model)
                 .injectEnvironmentObjects(service: details.accountService, model: model, focusState: $focusedDataEntry)
         }
     }
diff --git a/Sources/SpeziAccount/Views/AccountOverview/AccountOverviewHeader.swift b/Sources/SpeziAccount/Views/AccountOverview/AccountOverviewHeader.swift
index af57398c..d6340751 100644
--- a/Sources/SpeziAccount/Views/AccountOverview/AccountOverviewHeader.swift
+++ b/Sources/SpeziAccount/Views/AccountOverview/AccountOverviewHeader.swift
@@ -21,11 +21,10 @@ struct AccountOverviewHeader: View {
                     UserProfileView(name: profileViewName)
                         .frame(height: 90)
                 } else {
-                    Image(systemName: "person.circle.fill")
+                    Image(systemName: "person.crop.circle.fill")
                         .resizable()
                         .frame(width: 40, height: 40)
-                        .symbolRenderingMode(.hierarchical)
-                        .foregroundColor(Color(.systemGray))
+                        .foregroundColor(Color(.systemGray3))
                         .accessibilityHidden(true)
                 }
             }
diff --git a/Sources/SpeziAccount/Views/AccountOverview/AccountOverviewSections.swift b/Sources/SpeziAccount/Views/AccountOverview/AccountOverviewSections.swift
index e6268893..6ae83af0 100644
--- a/Sources/SpeziAccount/Views/AccountOverview/AccountOverviewSections.swift
+++ b/Sources/SpeziAccount/Views/AccountOverview/AccountOverviewSections.swift
@@ -122,19 +122,8 @@ struct AccountOverviewSections<AdditionalSections: View>: View {
                 // sync the edit mode with the outer view
                 isEditing = newValue
             }
-        
-        Section {
-            NavigationLink {
-                NameOverview(model: model, details: accountDetails)
-            } label: {
-                model.accountIdentifierLabel(configuration: account.configuration, userIdType: accountDetails.userIdType)
-            }
-            NavigationLink {
-                SecurityOverview(model: model, details: accountDetails)
-            } label: {
-                model.accountSecurityLabel(account.configuration)
-            }
-        }
+
+        defaultSections
         
         sectionsView
             .injectEnvironmentObjects(service: service, model: model, focusState: $focusedDataEntry)
@@ -163,6 +152,31 @@ struct AccountOverviewSections<AdditionalSections: View>: View {
                 .frame(maxWidth: .infinity, alignment: .center)
         }
     }
+
+    @ViewBuilder private var defaultSections: some View {
+        let displayName = model.displaysNameDetails()
+        let displaySecurity = model.displaysSignInSecurityDetails(accountDetails)
+
+        if displayName || displaySecurity {
+            Section {
+                if displayName {
+                    NavigationLink {
+                        NameOverview(model: model, details: accountDetails)
+                    } label: {
+                        model.accountIdentifierLabel(configuration: account.configuration, userIdType: accountDetails.userIdType)
+                    }
+                }
+
+                if displaySecurity {
+                    NavigationLink {
+                        SecurityOverview(model: model, details: accountDetails)
+                    } label: {
+                        Text("SIGN_IN_AND_SECURITY", bundle: .module)
+                    }
+                }
+            }
+        }
+    }
     
     @ViewBuilder private var sectionsView: some View {
         ForEach(model.editableAccountKeys(details: accountDetails).elements, id: \.key) { category, accountKeys in
@@ -174,7 +188,7 @@ struct AccountOverviewSections<AdditionalSections: View>: View {
                     }
                     
                     ForEach(forEachWrappers, id: \.id) { wrapper in
-                        AccountKeyEditRow(details: accountDetails, for: wrapper.accountKey, model: model)
+                        AccountKeyOverviewRow(details: accountDetails, for: wrapper.accountKey, model: model)
                     }
                         .onDelete { indexSet in
                             model.deleteAccountKeys(at: indexSet, in: accountKeys)
diff --git a/Sources/SpeziAccount/Views/AccountOverview/NameOverview.swift b/Sources/SpeziAccount/Views/AccountOverview/NameOverview.swift
index c41962cd..2ef57ec5 100644
--- a/Sources/SpeziAccount/Views/AccountOverview/NameOverview.swift
+++ b/Sources/SpeziAccount/Views/AccountOverview/NameOverview.swift
@@ -19,34 +19,32 @@ struct NameOverview: View {
 
     var body: some View {
         Form {
-            Section {
-                NavigationLink {
-                    SingleEditView<UserIdKey>(model: model, details: accountDetails)
-                } label: {
-                    UserIdKey.dataDisplayViewWithCurrentStoredValue(from: accountDetails)
-                }
-            }
-
-            Section {
-                NavigationLink {
-                    SingleEditView<PersonNameKey>(model: model, details: accountDetails)
-                } label: {
-                    if let name = accountDetails.name {
-                        PersonNameKey.DataDisplay(name)
-                    } else {
-                        HStack {
-                            Text(PersonNameKey.name)
-                                .accessibilityHidden(true)
-                            Spacer()
-                            Text("VALUE_ADD \(PersonNameKey.name)", bundle: .module)
-                                .foregroundColor(.secondary)
-                        }
+            let forEachWrappers = model.namesOverviewKeys(details: accountDetails)
+                .map { ForEachAccountKeyWrapper($0) }
+
+            ForEach(forEachWrappers, id: \.id) { wrapper in
+                Section {
+                    NavigationLink {
+                        wrapper.accountKey.singleEditView(model: model, details: accountDetails)
+                    } label: {
+                        if let view = wrapper.accountKey.dataDisplayViewWithCurrentStoredValue(from: accountDetails) {
+                            view
+                        } else {
+                            HStack {
+                                Text(wrapper.accountKey.name)
+                                    .accessibilityHidden(true)
+                                Spacer()
+                                Text("VALUE_ADD \(wrapper.accountKey.name)", bundle: .module)
+                                    .foregroundColor(.secondary)
+                            }
                             .accessibilityElement(children: .combine)
+                        }
+                    }
+                } header: {
+                    if wrapper.accountKey == PersonNameKey.self,
+                       let title = PersonNameKey.category.categoryTitle {
+                        Text(title)
                     }
-                }
-            } header: {
-                if let title = PersonNameKey.category.categoryTitle {
-                    Text(title)
                 }
             }
         }
diff --git a/Sources/SpeziAccount/Views/AccountOverview/SecurityOverview.swift b/Sources/SpeziAccount/Views/AccountOverview/SecurityOverview.swift
index 060996c8..d44a222d 100644
--- a/Sources/SpeziAccount/Views/AccountOverview/SecurityOverview.swift
+++ b/Sources/SpeziAccount/Views/AccountOverview/SecurityOverview.swift
@@ -29,32 +29,37 @@ struct SecurityOverview: View {
 
     var body: some View {
         Form {
-            Button("Change Password", action: {
-                presentingPasswordChangeSheet = true
-            })
-                .sheet(isPresented: $presentingPasswordChangeSheet) {
-                    PasswordChangeSheet(model: model, details: accountDetails)
-                }
-
-            // we place every account key of the `.credentials` section except the userId and password below
+            // we place every account key of the `.credentials` section except the userId
             let forEachWrappers = model.accountKeys(by: .credentials, using: accountDetails)
-                .filter { $0 != UserIdKey.self && $0 != PasswordKey.self }
+                .filter { !$0.isHiddenCredential }
                 .map { ForEachAccountKeyWrapper($0) }
 
 
             ForEach(forEachWrappers, id: \.id) { wrapper in
                 Section {
-                    // This view currently doesn't implement an EditMode. Current intention is that the
-                    // DataDisplay view of `.credentials` account values just build toggles or NavigationLinks
-                    // to manage and change the respective account value.
-                    AccountKeyEditRow(details: accountDetails, for: wrapper.accountKey, model: model)
+                    if wrapper.accountKey == PasswordKey.self {
+                        // we have a special case for the PasswordKey, as we currently don't expose the capabilities required to the subviews!
+                        Button(action: {
+                            presentingPasswordChangeSheet = true
+                        }) {
+                            Text("CHANGE_PASSWORD", bundle: .module)
+                        }
+                            .sheet(isPresented: $presentingPasswordChangeSheet) {
+                                PasswordChangeSheet(model: model, details: accountDetails)
+                            }
+                    } else {
+                        // This view currently doesn't implement an EditMode. Current intention is that the
+                        // DataDisplay view of `.credentials` account values just build toggles or NavigationLinks
+                        // to manage and change the respective account value.
+                        AccountKeyOverviewRow(details: accountDetails, for: wrapper.accountKey, model: model)
+                    }
                 }
             }
                 .injectEnvironmentObjects(service: service, model: model, focusState: $focusedDataEntry)
                 .environment(\.defaultErrorDescription, model.defaultErrorDescription)
         }
             .viewStateAlert(state: $viewState)
-            .navigationTitle(model.accountSecurityLabel(account.configuration))
+            .navigationTitle(Text("SIGN_IN_AND_SECURITY", bundle: .module))
             .navigationBarTitleDisplayMode(.inline)
             .onDisappear {
                 model.resetModelState()
diff --git a/Sources/SpeziAccount/Views/AccountOverview/SingleEditView.swift b/Sources/SpeziAccount/Views/AccountOverview/SingleEditView.swift
index 092d3cfc..0656542f 100644
--- a/Sources/SpeziAccount/Views/AccountOverview/SingleEditView.swift
+++ b/Sources/SpeziAccount/Views/AccountOverview/SingleEditView.swift
@@ -35,7 +35,7 @@ struct SingleEditView<Key: AccountKey>: View {
     var body: some View {
         Form {
             VStack {
-                Key.dataEntryViewWithStoredValue(details: accountDetails, for: ModifiedAccountDetails.self)
+                Key.dataEntryViewWithStoredValueOrInitial(details: accountDetails, for: ModifiedAccountDetails.self)
             }
         }
             .navigationTitle(Text(Key.self == UserIdKey.self ? accountDetails.userIdType.localizedStringResource : Key.name))
diff --git a/Sources/SpeziAccount/Views/AccountSetup/DefaultAccountSetupHeader.swift b/Sources/SpeziAccount/Views/AccountSetup/DefaultAccountSetupHeader.swift
index 5dbd0437..3fa89054 100644
--- a/Sources/SpeziAccount/Views/AccountSetup/DefaultAccountSetupHeader.swift
+++ b/Sources/SpeziAccount/Views/AccountSetup/DefaultAccountSetupHeader.swift
@@ -27,10 +27,10 @@ public struct DefaultAccountSetupHeader: View {
                 .padding(.top, 30)
 
             Group {
-                if !account.signedIn || setupState == .loadingExistingAccount {
-                    Text("ACCOUNT_WELCOME_SUBTITLE", bundle: .module)
-                } else {
+                if account.signedIn, case .generic = setupState {
                     Text("ACCOUNT_WELCOME_SIGNED_IN_SUBTITLE", bundle: .module)
+                } else {
+                    Text("ACCOUNT_WELCOME_SUBTITLE", bundle: .module)
                 }
             }
                 .multilineTextAlignment(.center)
diff --git a/Sources/SpeziAccount/Views/AccountSetup/FollowUpInfoSheet.swift b/Sources/SpeziAccount/Views/AccountSetup/FollowUpInfoSheet.swift
new file mode 100644
index 00000000..2cd6cf24
--- /dev/null
+++ b/Sources/SpeziAccount/Views/AccountSetup/FollowUpInfoSheet.swift
@@ -0,0 +1,155 @@
+//
+// This source file is part of the Stanford Spezi open-source project
+//
+// SPDX-FileCopyrightText: 2023 Stanford University and the project authors (see CONTRIBUTORS.md)
+//
+// SPDX-License-Identifier: MIT
+//
+
+import OrderedCollections
+import SpeziViews
+import SwiftUI
+
+
+struct FollowUpInfoSheet: View {
+    private let accountDetails: AccountDetails
+    private let accountKeyByCategory: OrderedDictionary<AccountKeyCategory, [any AccountKey.Type]>
+
+    private var service: any AccountService {
+        accountDetails.accountService
+    }
+
+    @Environment(\.logger) private var logger
+    @Environment(\.dismiss) private var dismiss
+
+    @EnvironmentObject private var account: Account
+
+    @StateObject private var detailsBuilder = ModifiedAccountDetails.Builder()
+    @StateObject private var validationEngines = ValidationEngines<String>()
+
+    @State private var viewState: ViewState = .idle
+    @FocusState private var focusedDataEntry: String?
+
+    @State private var presentingCancellationConfirmation = false
+
+
+    var body: some View {
+        form
+            .interactiveDismissDisabled(true)
+            .viewStateAlert(state: $viewState)
+            .toolbar {
+                ToolbarItem(placement: .cancellationAction) {
+                    Button(action: {
+                        presentingCancellationConfirmation = true
+                    }) {
+                        Text("CANCEL", bundle: .module)
+                    }
+                }
+            }
+            .confirmationDialog(
+                Text("CONFIRMATION_DISCARD_ADDITIONAL_INFO_TITLE", bundle: .module),
+                isPresented: $presentingCancellationConfirmation,
+                titleVisibility: .visible
+            ) {
+                Button(role: .destructive, action: {
+                    dismiss()
+                }) {
+                    Text("CONFIRMATION_DISCARD_ADDITIONAL_INFO", bundle: .module)
+                }
+                Button(role: .cancel, action: {}) {
+                    Text("CONFIRMATION_KEEP_EDITING", bundle: .module)
+                }
+            }
+    }
+
+    @ViewBuilder private var form: some View {
+        Form {
+            VStack {
+                Image(systemName: "person.crop.rectangle.badge.plus")
+                    .foregroundColor(.accentColor)
+                    .symbolRenderingMode(.multicolor)
+                    .font(.custom("XXL", size: 50, relativeTo: .title))
+                    .accessibilityHidden(true)
+                Text("FOLLOW_UP_INFORMATION_TITLE", bundle: .module)
+                    .accessibilityAddTraits(.isHeader)
+                    .font(.title)
+                    .bold()
+                    .padding(.bottom, 4)
+                Text("FOLLOW_UP_INFORMATION_INSTRUCTIONS", bundle: .module)
+                    .padding([.leading, .trailing], 25)
+            }
+                .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
+                .listRowBackground(Color.clear)
+                .padding(.top, -3)
+                .multilineTextAlignment(.center)
+                .frame(maxWidth: .infinity)
+
+            SignupSectionsView(for: ModifiedAccountDetails.self, service: service, sections: accountKeyByCategory)
+                .environment(\.accountServiceConfiguration, service.configuration)
+                .environment(\.accountViewType, .signup)
+                .environmentObject(detailsBuilder)
+                .environmentObject(validationEngines)
+                .environmentObject(FocusStateObject(focusedField: $focusedDataEntry))
+
+            AsyncButton(state: $viewState, action: completeButtonAction) {
+                Text("FOLLOW_UP_INFORMATION_COMPLETE", bundle: .module)
+                    .padding(16)
+                    .frame(maxWidth: .infinity)
+            }
+                .buttonStyle(.borderedProminent)
+                .padding()
+                .padding(-36)
+                .listRowBackground(Color.clear)
+                .disabled(!validationEngines.allInputValid)
+        }
+            .environment(\.defaultErrorDescription, .init("ACCOUNT_OVERVIEW_EDIT_DEFAULT_ERROR", bundle: .atURL(from: .module)))
+    }
+
+
+    init(details: AccountDetails, requiredKeys: [any AccountKey.Type]) {
+        self.accountDetails = details
+        self.accountKeyByCategory = requiredKeys.reduce(into: [:]) { result, key in
+            result[key.category, default: []] += [key]
+        }
+    }
+
+
+    private func completeButtonAction() async throws {
+        guard validationEngines.validateSubviews(focusState: $focusedDataEntry) else {
+            logger.debug("Failed to save updated account information. Validation failed!")
+            return
+        }
+
+        focusedDataEntry = nil
+
+        let modifiedDetails = try detailsBuilder.build(validation: true)
+        let removedDetails = RemovedAccountDetails.Builder().build()
+
+        let modifications = AccountModifications(modifiedDetails: modifiedDetails, removedAccountDetails: removedDetails)
+
+        logger.debug("Finished additional account setup. Saving \(detailsBuilder.count) changes!")
+
+        try await service.updateAccountDetails(modifications)
+
+        dismiss()
+    }
+}
+
+
+#if DEBUG
+struct FollowUpInfoSheet_Previews: PreviewProvider {
+    static let details = AccountDetails.Builder()
+        .set(\.userId, value: "lelandstanford@stanford.edu")
+
+    static let account = Account(building: details, active: MockUserIdPasswordAccountService())
+
+    static var previews: some View {
+        NavigationStack {
+            if let details = account.details {
+                FollowUpInfoSheet(details: details, requiredKeys: [PersonNameKey.self])
+            }
+        }
+            .environmentObject(account)
+    }
+}
+#endif
diff --git a/Sources/SpeziAccount/Views/AccountSetup/SignupSectionsView.swift b/Sources/SpeziAccount/Views/AccountSetup/SignupSectionsView.swift
new file mode 100644
index 00000000..9d16e9f7
--- /dev/null
+++ b/Sources/SpeziAccount/Views/AccountSetup/SignupSectionsView.swift
@@ -0,0 +1,73 @@
+//
+// This source file is part of the Stanford Spezi open-source project
+//
+// SPDX-FileCopyrightText: 2023 Stanford University and the project authors (see CONTRIBUTORS.md)
+//
+// SPDX-License-Identifier: MIT
+//
+
+import OrderedCollections
+import SwiftUI
+
+
+/// This views renders the sections for the signup or signup-like views.
+///
+/// The view and it's subviews typically expect the following environment objects:
+/// - The global ``Account`` object
+/// - The internal `FocusStateObject` to pass down a `FocusState` (for the PersonNameKey implementation).
+/// - An instance of ``AccountValuesBuilder`` according to the generic ``AccountValues`` type.
+/// - An ``ValidationEngines`` object.
+/// - The ``SwiftUI/EnvironmentValues/accountServiceConfiguration`` environment variable.
+/// - The ``SwiftUI/EnvironmentValues/accountViewType`` environment variable.
+struct SignupSectionsView<Storage: AccountValues>: View {
+    private let service: any AccountService
+    private let sections: OrderedDictionary<AccountKeyCategory, [any AccountKey.Type]>
+    private let storageType: Storage.Type
+
+    @EnvironmentObject private var account: Account
+
+    var body: some View {
+        // OrderedDictionary `elements` conforms to RandomAccessCollection so we can directly use it
+        ForEach(sections.elements, id: \.key) { category, accountKeys in
+            Section {
+                // the array doesn't change, so its fine to rely on the indices as identifiers
+                ForEach(accountKeys.indices, id: \.self) { index in
+                    VStack {
+                        accountKeys[index].emptyDataEntryView(for: storageType)
+                    }
+                }
+            } header: {
+                if let title = category.categoryTitle {
+                    Text(title)
+                }
+            } footer: {
+                if category == .credentials && account.configuration[PasswordKey.self] != nil {
+                    PasswordValidationRuleFooter(configuration: service.configuration)
+                }
+            }
+        }
+    }
+
+    init(for storageType: Storage.Type, service: any AccountService, sections: OrderedDictionary<AccountKeyCategory, [any AccountKey.Type]>) {
+        self.service = service
+        self.sections = sections
+        self.storageType = storageType
+    }
+}
+
+
+#if DEBUG
+struct SignupSectionsView_Previews: PreviewProvider {
+    private static let service = MockUserIdPasswordAccountService()
+
+    static var previews: some View {
+        Form {
+            SignupSectionsView(for: SignupDetails.self, service: service, sections: [
+                .credentials: [UserIdKey.self, PasswordKey.self],
+                .name: [PersonNameKey.self]
+            ])
+        }
+            .environmentObject(Account(service))
+    }
+}
+#endif
diff --git a/Sources/SpeziAccount/Views/AccountSummaryBox.swift b/Sources/SpeziAccount/Views/AccountSummaryBox.swift
index fd9920cd..78e89af7 100644
--- a/Sources/SpeziAccount/Views/AccountSummaryBox.swift
+++ b/Sources/SpeziAccount/Views/AccountSummaryBox.swift
@@ -21,11 +21,10 @@ public struct AccountSummaryBox: View {
                     UserProfileView(name: profileViewName)
                         .frame(height: 40)
                 } else {
-                    Image(systemName: "person.circle.fill")
+                    Image(systemName: "person.crop.circle.fill")
                         .resizable()
                         .frame(width: 40, height: 40)
-                        .symbolRenderingMode(.hierarchical)
-                        .foregroundColor(Color(.systemGray))
+                        .foregroundColor(Color(.systemGray3))
                         .accessibilityHidden(true)
                 }
             }
diff --git a/Sources/SpeziAccount/Views/DataEntry/GeneralizedDataEntryView.swift b/Sources/SpeziAccount/Views/DataEntry/GeneralizedDataEntryView.swift
index a1940705..f2b2c257 100644
--- a/Sources/SpeziAccount/Views/DataEntry/GeneralizedDataEntryView.swift
+++ b/Sources/SpeziAccount/Views/DataEntry/GeneralizedDataEntryView.swift
@@ -26,10 +26,15 @@ private protocol GeneralizedStringEntryView {
 ///     ``DataEntryView/Key``, a  ``SwiftUI/View/managedValidation(input:for:rules:)-5gj5g`` modifier is automatically injected. One can easily override
 ///     the modified by declaring a custom one in the subview.
 public struct GeneralizedDataEntryView<Wrapped: DataEntryView, Values: AccountValues>: View {
+    private var dataHookId: String {
+        "DataHook-\(Wrapped.Key.self)"
+    }
+
     @EnvironmentObject private var account: Account
 
     @EnvironmentObject private var focusState: FocusStateObject
     @EnvironmentObject private var detailsBuilder: AccountValuesBuilder<Values>
+    @EnvironmentObject private var engines: ValidationEngines<String>
 
     @Environment(\.accountServiceConfiguration) private var configuration
     @Environment(\.accountViewType) private var viewType
@@ -61,9 +66,16 @@ public struct GeneralizedDataEntryView<Wrapped: DataEntryView, Values: AccountVa
                 // values like `GenderIdentity` provide a default value a user might not want to change
                 if viewType?.enteringNewData == true,
                    case let .default(value) = Wrapped.Key.initialValue {
-                    detailsBuilder.set(Wrapped.Key.self, value: value)
+                    engines.register(id: dataHookId) {
+                        if detailsBuilder.get(Wrapped.Key.self) == nil {
+                            detailsBuilder.set(Wrapped.Key.self, value: value)
+                        }
+                    }
                 }
             }
+            .onDisappear {
+                engines.remove(hook: dataHookId)
+            }
             .onChange(of: value) { newValue in
                 // ensure parent view has access to the latest value
                 if viewType?.enteringNewData == true,
diff --git a/Sources/SpeziAccount/Views/SignupForm.swift b/Sources/SpeziAccount/Views/SignupForm.swift
index 437aed9d..7f45f516 100644
--- a/Sources/SpeziAccount/Views/SignupForm.swift
+++ b/Sources/SpeziAccount/Views/SignupForm.swift
@@ -11,6 +11,31 @@ import SpeziViews
 import SwiftUI
 
 
+struct DefaultSignupFormHeader: View {
+    var body: some View {
+        VStack {
+            Image(systemName: "person.fill.badge.plus")
+                .foregroundColor(.accentColor)
+                .symbolRenderingMode(.multicolor)
+                .font(.custom("XXL", size: 50, relativeTo: .title))
+                .accessibilityHidden(true)
+            Text("UP_SIGNUP_HEADER", bundle: .module)
+                .accessibilityAddTraits(.isHeader)
+                .font(.title)
+                .bold()
+                .padding(.bottom, 4)
+            Text("UP_SIGNUP_INSTRUCTIONS", bundle: .module)
+                .padding([.leading, .trailing], 25)
+        }
+            .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
+            .listRowBackground(Color.clear)
+            .padding(.top, -3)
+            .multilineTextAlignment(.center)
+            .frame(maxWidth: .infinity)
+    }
+}
+
+
 /// A generalized signup form used with arbitrary ``AccountService`` implementations.
 ///
 /// A `Form` that collects all configured account values (a ``AccountValueConfiguration`` supplied to ``AccountConfiguration``)
@@ -32,21 +57,23 @@ public struct SignupForm<Service: AccountService, Header: View>: View {
 
     @State private var presentingCloseConfirmation = false
 
-    private var signupValuesBySections: OrderedDictionary<AccountKeyCategory, [any AccountKey.Type]> {
-        account.configuration.reduce(into: [:]) { result, configuration in
-            guard configuration.requirement != .supported else {
-                // we only show required and collected values in signup
-                return
-            }
+    private var accountKeyByCategory: OrderedDictionary<AccountKeyCategory, [any AccountKey.Type]> {
+        var result = account.configuration.allCategorized(filteredBy: [.required, .collected])
 
-            result[configuration.key.category, default: []] += [configuration.key]
+        // patch the user configured account values with account values additionally required by
+        for entry in service.configuration.requiredAccountKeys {
+            let key = entry.key
+            if !result[key.category, default: []].contains(where: { $0 == key }) {
+                result[key.category, default: []].append(key)
+            }
         }
+
+        return result
     }
 
 
     public var body: some View {
         form
-            .navigationTitle(Text("UP_SIGNUP", bundle: .module))
             .disableDismissiveActions(isProcessing: viewState)
             .viewStateAlert(state: $viewState)
             .interactiveDismissDisabled(!signupDetailsBuilder.isEmpty)
@@ -83,7 +110,7 @@ public struct SignupForm<Service: AccountService, Header: View>: View {
         Form {
             header
 
-            sectionsView
+            SignupSectionsView(for: SignupDetails.self, service: service, sections: accountKeyByCategory)
                 .environment(\.accountServiceConfiguration, service.configuration)
                 .environment(\.accountViewType, .signup)
                 .environmentObject(signupDetailsBuilder)
@@ -96,40 +123,18 @@ public struct SignupForm<Service: AccountService, Header: View>: View {
                     .frame(maxWidth: .infinity)
             }
                 .buttonStyle(.borderedProminent)
-                .disabled(!validationEngines.allInputValid)
                 .padding()
                 .padding(-36)
                 .listRowBackground(Color.clear)
+                .disabled(!validationEngines.allInputValid)
         }
             .environment(\.defaultErrorDescription, .init("UP_SIGNUP_FAILED_DEFAULT_ERROR", bundle: .atURL(from: .module)))
     }
 
-    @ViewBuilder var sectionsView: some View {
-        // OrderedDictionary `elements` conforms to RandomAccessCollection so we can directly use it
-        ForEach(signupValuesBySections.elements, id: \.key) { category, accountKeys in
-            Section {
-                // the array doesn't change, so its fine to rely on the indices as identifiers
-                ForEach(accountKeys.indices, id: \.self) { index in
-                    VStack {
-                        accountKeys[index].emptyDataEntryView(for: SignupDetails.self)
-                    }
-                }
-            } header: {
-                if let title = category.categoryTitle {
-                    Text(title)
-                }
-            } footer: {
-                if category == .credentials && account.configuration[PasswordKey.self] != nil {
-                    PasswordValidationRuleFooter(configuration: service.configuration)
-                }
-            }
-        }
-    }
-
 
-    init(using service: Service) where Header == Text {
+    init(using service: Service) where Header == DefaultSignupFormHeader {
         self.service = service
-        self.header = Text("UP_SIGNUP_INSTRUCTIONS", bundle: .module)
+        self.header = DefaultSignupFormHeader()
     }
 
     init(service: Service, @ViewBuilder header: () -> Header) {
@@ -145,9 +150,9 @@ public struct SignupForm<Service: AccountService, Header: View>: View {
 
         focusedDataEntry = nil
 
-        let request: SignupDetails = try signupDetailsBuilder.build(checking: account.configuration)
+        let details: SignupDetails = try signupDetailsBuilder.build(checking: account.configuration)
 
-        try await service.signUp(signupDetails: request)
+        try await service.signUp(signupDetails: details)
 
         // go back if the view doesn't update anyway
         dismiss()
@@ -160,13 +165,10 @@ struct DefaultUserIdPasswordSignUpView_Previews: PreviewProvider {
     static let accountService = MockUserIdPasswordAccountService()
 
     static var previews: some View {
-        Text("")
-            .sheet(isPresented: .constant(true)) {
-                NavigationStack {
-                    SignupForm(using: accountService)
-                }
-                    .environmentObject(Account(accountService))
-            }
+        NavigationStack {
+            SignupForm(using: accountService)
+        }
+            .environmentObject(Account(accountService))
     }
 }
 #endif
diff --git a/Tests/UITests/TestApp/AccountTests/AccountTestsView.swift b/Tests/UITests/TestApp/AccountTests/AccountTestsView.swift
index 7ea8b1fd..409954a9 100644
--- a/Tests/UITests/TestApp/AccountTests/AccountTestsView.swift
+++ b/Tests/UITests/TestApp/AccountTests/AccountTestsView.swift
@@ -33,41 +33,43 @@ struct AccountTestsView: View {
                 Button("Account Overview") {
                     showOverview = true
                 }
-            }
-            .navigationTitle("Spezi Account")
-            .sheet(isPresented: $showSetup) {
-                NavigationStack {
-                    AccountSetup {
-                        finishButton
-                    }
-                    .toolbar {
-                        toolbar(closing: $showSetup)
+                Button("Account Logout", role: .destructive) {
+                    Task {
+                        try? await account.details?.accountService.logout()
                     }
                 }
+                    .disabled(!account.signedIn)
             }
-            .sheet(isPresented: $showOverview) {
-                NavigationStack {
-                    AccountOverview(isEditing: $isEditing) {
-                        NavigationLink {
-                            Text("")
-                                .navigationTitle(Text("Package Dependencies"))
-                        } label: {
-                            Text("License Information")
-                        }
-                    }
+                .navigationTitle("Spezi Account")
+                .sheet(isPresented: $showSetup) {
+                    setupSheet()
                 }
-                .toolbar {
-                    toolbar(closing: $showOverview)
+                .sheet(isPresented: $showOverview) {
+                    overviewSheet
                 }
-            }
         }
-        .onChange(of: account.signedIn) { newValue in
-            if newValue {
-                showSetup = false
+            .accountRequired(features.accountRequiredModifier) {
+                setupSheet(closeable: false)
             }
+            .verifyRequiredAccountDetails(features.verifyRequiredDetails)
+    }
+
+    @ViewBuilder var overviewSheet: some View {
+        NavigationStack {
+            AccountOverview(isEditing: $isEditing) {
+                NavigationLink {
+                    Text("")
+                        .navigationTitle(Text("Package Dependencies"))
+                } label: {
+                    Text("License Information")
+                }
+            }
+        }
+        .toolbar {
+            toolbar(closing: $showOverview)
         }
     }
-    
+
     @ViewBuilder var header: some View {
         if let details = account.details {
             Section("Account Details") {
@@ -89,10 +91,26 @@ struct AccountTestsView: View {
             Text("Finish")
                 .frame(maxWidth: .infinity, minHeight: 38)
         })
-        .buttonStyle(.borderedProminent)
+            .buttonStyle(.borderedProminent)
     }
 
 
+    @ViewBuilder
+    func setupSheet(closeable: Bool = true) -> some View {
+        NavigationStack {
+            AccountSetup { _ in
+                showSetup = false
+            } continue: {
+                finishButton
+            }
+                .toolbar {
+                    if closeable {
+                        toolbar(closing: $showSetup)
+                    }
+                }
+        }
+    }
+
     @ToolbarContentBuilder
     func toolbar(closing flag: Binding<Bool>) -> some ToolbarContent {
         if !isEditing {
diff --git a/Tests/UITests/TestApp/AccountTests/TestAccountConfiguration.swift b/Tests/UITests/TestApp/AccountTests/TestAccountConfiguration.swift
index 4d45ef10..e6bf3d6d 100644
--- a/Tests/UITests/TestApp/AccountTests/TestAccountConfiguration.swift
+++ b/Tests/UITests/TestApp/AccountTests/TestAccountConfiguration.swift
@@ -18,15 +18,15 @@ final class TestAccountConfiguration: Component {
     init(features: Features) {
         switch features.serviceType {
         case .mail:
-            accountServices = [TestAccountService(.emailAddress, defaultAccount: features.defaultCredentials)]
+            accountServices = [TestAccountService(.emailAddress, defaultAccount: features.defaultCredentials, noName: features.noName)]
         case .both:
             accountServices = [
-                TestAccountService(.emailAddress, defaultAccount: features.defaultCredentials),
+                TestAccountService(.emailAddress, defaultAccount: features.defaultCredentials, noName: features.noName),
                 TestAccountService(.username)
             ]
         case .withIdentityProvider:
             accountServices = [
-                TestAccountService(.emailAddress, defaultAccount: features.defaultCredentials),
+                TestAccountService(.emailAddress, defaultAccount: features.defaultCredentials, noName: features.noName),
                 MockSignInWithAppleProvider()
             ]
         case .empty:
diff --git a/Tests/UITests/TestApp/AccountTests/TestAccountService.swift b/Tests/UITests/TestApp/AccountTests/TestAccountService.swift
index 040782c5..609c5ebb 100644
--- a/Tests/UITests/TestApp/AccountTests/TestAccountService.swift
+++ b/Tests/UITests/TestApp/AccountTests/TestAccountService.swift
@@ -13,12 +13,13 @@ actor TestAccountService: UserIdPasswordAccountService {
     nonisolated let configuration: AccountServiceConfiguration
     private let defaultUserId: String
     private let defaultAccountOnConfigure: Bool
+    private var excludeName: Bool
 
     @AccountReference var account: Account
     var registeredUser: UserStorage // simulates the backend
 
 
-    init(_ type: UserIdType, defaultAccount: Bool = false) {
+    init(_ type: UserIdType, defaultAccount: Bool = false, noName: Bool = false) {
         configuration = AccountServiceConfiguration(
             name: "\(type.localizedStringResource) and Password",
             supportedKeys: .exactly(UserStorage.supportedKeys)
@@ -32,6 +33,7 @@ actor TestAccountService: UserIdPasswordAccountService {
 
         defaultUserId = type == .emailAddress ? UserStorage.defaultEmail : UserStorage.defaultUsername
         self.defaultAccountOnConfigure = defaultAccount
+        self.excludeName = noName
         registeredUser = UserStorage(userId: defaultUserId)
     }
 
@@ -81,11 +83,20 @@ actor TestAccountService: UserIdPasswordAccountService {
     }
 
     func updateUser() async throws {
-        let details = AccountDetails.Builder()
+        let builder = AccountDetails.Builder()
+            .set(\.accountId, value: registeredUser.accountId.uuidString)
             .set(\.userId, value: registeredUser.userId)
-            .set(\.name, value: registeredUser.name)
             .set(\.genderIdentity, value: registeredUser.genderIdentity)
             .set(\.dateOfBirth, value: registeredUser.dateOfBirth)
+            .set(\.biography, value: registeredUser.biography)
+
+        if !self.excludeName {
+            builder.set(\.name, value: registeredUser.name)
+        } else {
+            excludeName = false
+        }
+
+        let details = builder
             .build(owner: self)
 
         try await account.supplyUserDetails(details)
diff --git a/Tests/UITests/TestApp/AccountTests/UserStorage.swift b/Tests/UITests/TestApp/AccountTests/UserStorage.swift
index 755c40fd..7e09c246 100644
--- a/Tests/UITests/TestApp/AccountTests/UserStorage.swift
+++ b/Tests/UITests/TestApp/AccountTests/UserStorage.swift
@@ -18,6 +18,7 @@ struct UserStorage {
         .day(.twoDigits)
 
     static let supportedKeys = AccountKeyCollection {
+        \.accountId
         \.userId
         \.password
         \.name
@@ -28,20 +29,24 @@ struct UserStorage {
     static let defaultUsername = "lelandstanford"
     static let defaultEmail = "lelandstanford@stanford.edu"
 
+    var accountId: UUID
     var userId: String
     var password: String
     var name: PersonNameComponents?
     var genderIdentity: GenderIdentity?
     var dateOfBirth: Date?
+    var biography: String?
     
     
-    init(
+    init( // swiftlint:disable:this function_default_parameter_at_end
+        accountId: UUID = UUID(),
         userId: String,
         password: String = "StanfordRocks123!",
         name: PersonNameComponents? = PersonNameComponents(givenName: "Leland", familyName: "Stanford"),
         gender: GenderIdentity? = .male,
         dateOfBirth: Date? = try? Date("09.03.1824", strategy: dateStyle)
     ) {
+        self.accountId = accountId
         self.userId = userId
         self.password = password
         self.name = name
@@ -59,6 +64,7 @@ struct UserStorage {
         self.name = modifiedDetails.name ?? name
         self.genderIdentity = modifiedDetails.genderIdentity ?? genderIdentity
         self.dateOfBirth = modifiedDetails.dateOfBrith ?? dateOfBirth
+        self.biography = modifiedDetails.biography ?? biography
 
         // user Id cannot be removed!
         if removedKeys.name != nil {
@@ -70,5 +76,8 @@ struct UserStorage {
         if removedKeys.dateOfBrith != nil {
             self.dateOfBirth = nil
         }
+        if removedKeys.biography != nil {
+            self.biography = nil
+        }
     }
 }
diff --git a/Tests/UITests/TestApp/Features.swift b/Tests/UITests/TestApp/Features.swift
index b1680cb5..86df3d58 100644
--- a/Tests/UITests/TestApp/Features.swift
+++ b/Tests/UITests/TestApp/Features.swift
@@ -21,6 +21,7 @@ enum AccountServiceType: String, ExpressibleByArgument {
 enum AccountValueConfigurationType: String, ExpressibleByArgument {
     case `default`
     case allRequired
+    case allRequiredWithBio
 }
 
 
@@ -33,6 +34,12 @@ struct Features: ParsableArguments, EnvironmentKey {
     @Option(help: "Define which type of AccountValueConfiguration is used.") var configurationType: AccountValueConfigurationType = .default
 
     @Flag(help: "Control if the app should be populated with default credentials.") var defaultCredentials = false
+
+    @Flag(help: "Enable the AccountRequiredModifier.swift") var accountRequiredModifier = false
+
+    @Flag(help: "Enable the VerifyRequiredAccountDetailsModifier") var verifyRequiredDetails = false
+
+    @Flag(help: "Set no name by default.") var noName = false
 }
 
 
diff --git a/Tests/UITests/TestApp/TestApp.entitlements b/Tests/UITests/TestApp/TestApp.entitlements
new file mode 100644
index 00000000..a812db50
--- /dev/null
+++ b/Tests/UITests/TestApp/TestApp.entitlements
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>com.apple.developer.applesignin</key>
+	<array>
+		<string>Default</string>
+	</array>
+</dict>
+</plist>
diff --git a/Tests/UITests/TestApp/TestApp.entitlements.license b/Tests/UITests/TestApp/TestApp.entitlements.license
new file mode 100644
index 00000000..7f16969d
--- /dev/null
+++ b/Tests/UITests/TestApp/TestApp.entitlements.license
@@ -0,0 +1,5 @@
+This source file is part of the Stanford Spezi open-source project
+
+SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md)
+
+SPDX-License-Identifier: MIT
\ No newline at end of file
diff --git a/Tests/UITests/TestApp/TestAppDelegate.swift b/Tests/UITests/TestApp/TestAppDelegate.swift
index 9dce92a1..abc3e20c 100644
--- a/Tests/UITests/TestApp/TestAppDelegate.swift
+++ b/Tests/UITests/TestApp/TestAppDelegate.swift
@@ -6,9 +6,11 @@
 // SPDX-License-Identifier: MIT
 //
 
+import Foundation
 import Spezi
 import SpeziAccount
 
+
 class TestAppDelegate: SpeziAppDelegate {
     let features: Features = {
         do {
@@ -16,7 +18,7 @@ class TestAppDelegate: SpeziAppDelegate {
             return features
         } catch {
             print("Error: \(error)")
-            print("Verify the supplied command line arguments: " + CommandLine.arguments.dropFirst().joined(separator: " "))
+            print("Verify the supplied command line arguments: " + ProcessInfo.processInfo.arguments.dropFirst().joined(separator: " "))
             print(Features.helpMessage())
             return Features()
         }
@@ -27,7 +29,6 @@ class TestAppDelegate: SpeziAppDelegate {
         case .default:
             return [
                 .requires(\.userId),
-                .requires(\.password),
                 .collects(\.name),
                 .collects(\.genderIdentity),
                 .collects(\.dateOfBirth),
@@ -36,10 +37,18 @@ class TestAppDelegate: SpeziAppDelegate {
         case .allRequired:
             return [
                 .requires(\.userId),
-                .requires(\.password),
                 .requires(\.name),
                 .requires(\.genderIdentity),
-                .requires(\.dateOfBirth)
+                .requires(\.dateOfBirth),
+                .supports(\.biography) // that's special case for checking follow up info on e.g. login
+            ]
+        case .allRequiredWithBio:
+            return [
+                .requires(\.userId),
+                .requires(\.name),
+                .requires(\.genderIdentity),
+                .requires(\.dateOfBirth),
+                .requires(\.biography)
             ]
         }
     }
diff --git a/Tests/UITests/TestAppUITests/AccountOverviewTests.swift b/Tests/UITests/TestAppUITests/AccountOverviewTests.swift
index d29f131c..c0e60459 100644
--- a/Tests/UITests/TestAppUITests/AccountOverviewTests.swift
+++ b/Tests/UITests/TestAppUITests/AccountOverviewTests.swift
@@ -25,7 +25,7 @@ final class AccountOverviewTests: XCTestCase {
         overview.verifyExistence(text: "lelandstanford@stanford.edu")
 
         overview.verifyExistence(text: "Name, E-Mail Address")
-        overview.verifyExistence(text: "Password & Security")
+        overview.verifyExistence(text: "Sign-In & Security")
 
         overview.verifyExistence(text: "Gender Identity, Male")
 
@@ -213,13 +213,37 @@ final class AccountOverviewTests: XCTestCase {
         XCTAssertTrue(overview.staticTexts["L"].waitForExistence(timeout: 2.0)) // ensure the "account image" is updated accordingly
     }
 
+    func testAddName() throws {
+        let app = TestApp.launch(defaultCredentials: true, noName: true)
+        let overview = app.openAccountOverview()
+
+        overview.verifyExistence(text: "Name, E-Mail Address")
+
+        overview.tap(button: "Name, E-Mail Address")
+        sleep(2)
+        XCTAssertTrue(overview.navigationBars.staticTexts["Name, E-Mail Address"].waitForExistence(timeout: 6.0))
+
+        // open user id
+        overview.tap(button: "Add Name")
+        sleep(2)
+        XCTAssertFalse(overview.buttons["Done"].isEnabled)
+
+        try overview.enter(field: "enter first name", text: "Leland")
+        try overview.enter(field: "enter last name", text: "Stanford")
+
+        overview.tap(button: "Done")
+        sleep(3)
+
+        overview.verifyExistence(text: "Name, Leland Stanford")
+    }
+
     func testSecurityOverview() throws {
         let app = TestApp.launch(defaultCredentials: true)
         let overview = app.openAccountOverview()
 
-        overview.tap(button: "Password & Security")
+        overview.tap(button: "Sign-In & Security")
         sleep(2)
-        XCTAssertTrue(overview.navigationBars.staticTexts["Password & Security"].waitForExistence(timeout: 6.0))
+        XCTAssertTrue(overview.navigationBars.staticTexts["Sign-In & Security"].waitForExistence(timeout: 6.0))
 
         XCTAssertTrue(overview.buttons["Change Password"].waitForExistence(timeout: 2.0))
         overview.tap(button: "Change Password")
diff --git a/Tests/UITests/TestAppUITests/AccountSetupTests.swift b/Tests/UITests/TestAppUITests/AccountSetupTests.swift
index 5a27bdf0..25612098 100644
--- a/Tests/UITests/TestAppUITests/AccountSetupTests.swift
+++ b/Tests/UITests/TestAppUITests/AccountSetupTests.swift
@@ -160,7 +160,7 @@ final class AccountSetupTests: XCTestCase {
         XCTAssertEqual(signupView.staticTexts.matching(identifier: "This field cannot be empty.").count, 2)
 
         // not sure why, but text-field selection has issues due to the presented validation messages, so we exit a reenter to resolve this
-        setup = signupView.tapClose()
+        setup = try signupView.tapClose()
         signupView = setup.openSignup()
 
         // enter email with validation
@@ -184,7 +184,6 @@ final class AccountSetupTests: XCTestCase {
         XCTAssertTrue(overview.staticTexts[email].waitForExistence(timeout: 2.0))
         XCTAssertTrue(overview.staticTexts["Gender Identity"].waitForExistence(timeout: 0.5))
         XCTAssertTrue(overview.staticTexts["Choose not to answer"].waitForExistence(timeout: 0.5))
-        XCTAssertTrue(overview.images["Contact Photo"].waitForExistence(timeout: 0.5)) // verify the header works well without a name
     }
 
     func testNameValidation() throws {
@@ -287,7 +286,6 @@ final class AccountSetupTests: XCTestCase {
         XCTAssertTrue(overview.staticTexts[email].waitForExistence(timeout: 2.0))
         XCTAssertTrue(overview.staticTexts["Gender Identity"].waitForExistence(timeout: 0.5))
         XCTAssertTrue(overview.staticTexts["Choose not to answer"].waitForExistence(timeout: 0.5))
-        XCTAssertTrue(overview.images["Contact Photo"].waitForExistence(timeout: 0.5))
 
         overview.tap(button: "Name, E-Mail Address")
         sleep(2)
@@ -297,4 +295,39 @@ final class AccountSetupTests: XCTestCase {
         XCTAssertFalse(overview.staticTexts["Leland"].waitForExistence(timeout: 1.0))
         overview.verifyExistence(text: "Add Name")
     }
+
+    func testAdditionalInfoAfterLogin() throws {
+        let app = TestApp.launch(config: "allRequiredWithBio")
+
+        let setup = app.openAccountSetup()
+
+        try setup.login(email: Defaults.email, password: Defaults.password)
+
+        // verify the finish account setup view is popping up
+        XCTAssertTrue(setup.staticTexts["Finish Account Setup"].waitForExistence(timeout: 2.0))
+        XCTAssertTrue(setup.staticTexts["Please fill out the details below to complete your account setup."].waitForExistence(timeout: 0.5))
+
+        try setup.enter(field: "Biography", text: "Hello Stanford")
+        sleep(2)
+
+        setup.tap(button: "Complete")
+        sleep(3)
+
+        // verify we are back at the start screen
+        XCTAssertTrue(app.staticTexts[Defaults.email].waitForExistence(timeout: 2.0))
+    }
+
+    func testAccountRequiredModifier() throws {
+        let app = TestApp.launch(defaultCredentials: true, accountRequired: true)
+
+        app.tap(button: "Account Logout")
+
+        XCTAssertTrue(app.staticTexts["Your Account"].waitForExistence(timeout: 2.0))
+    }
+
+    func testVerifyRequiredAccountDetailsModifier() throws {
+        let app = TestApp.launch(config: "allRequiredWithBio", defaultCredentials: true, verifyAccountDetails: true)
+
+        XCTAssertTrue(app.staticTexts["Finish Account Setup"].waitForExistence(timeout: 2.0))
+    }
 }
diff --git a/Tests/UITests/TestAppUITests/Utils/SignupView.swift b/Tests/UITests/TestAppUITests/Utils/SignupView.swift
index 5953a1f4..01f6f0f5 100644
--- a/Tests/UITests/TestAppUITests/Utils/SignupView.swift
+++ b/Tests/UITests/TestAppUITests/Utils/SignupView.swift
@@ -17,7 +17,7 @@ struct SignupView: AccountValueView {
     }
 
     func verify() {
-        XCTAssertTrue(app.staticTexts["Please fill out the details below to create a new account."].waitForExistence(timeout: 6.0))
+        XCTAssertTrue(app.staticTexts["Please fill out the details below to create your new account."].waitForExistence(timeout: 6.0))
     }
 
     func fillForm(
@@ -62,9 +62,9 @@ struct SignupView: AccountValueView {
         }
     }
 
-    func tapClose(timeout: TimeInterval = 1.0, discardChangesIfAsked: Bool = true) -> TestableAccountSetup {
-        XCTAssertTrue(app.navigationBars["Signup"].buttons["Close"].waitForExistence(timeout: timeout))
-        app.navigationBars["Signup"].buttons["Close"].tap()
+    func tapClose(timeout: TimeInterval = 1.0, discardChangesIfAsked: Bool = true) throws -> TestableAccountSetup {
+        XCTAssertTrue(app.navigationBars.buttons["Close"].waitForExistence(timeout: timeout))
+        try XCTUnwrap(app.navigationBars.buttons.matching(identifier: "Close").allElementsBoundByIndex.last).tap()
 
         if discardChangesIfAsked && app.staticTexts["Are you sure you want to discard your input?"].waitForExistence(timeout: 2.0) {
             tap(button: "Discard Input")
diff --git a/Tests/UITests/TestAppUITests/Utils/TestApp.swift b/Tests/UITests/TestAppUITests/Utils/TestApp.swift
index c2b0d164..f6f58eb7 100644
--- a/Tests/UITests/TestAppUITests/Utils/TestApp.swift
+++ b/Tests/UITests/TestAppUITests/Utils/TestApp.swift
@@ -20,12 +20,18 @@ struct TestApp: TestableView {
         serviceType: String = "mail",
         config: String = "default",
         defaultCredentials: Bool = false,
+        accountRequired: Bool = false,
+        verifyAccountDetails: Bool = false,
+        noName: Bool = false,
         flags: String...
     ) -> TestApp {
         let app = XCUIApplication()
         app.launchArguments = ["--service-type", serviceType, "--configuration-type", config]
-        + (defaultCredentials ? ["--default-credentials"] : [])
-        + flags
+        app.launchArguments += (defaultCredentials ? ["--default-credentials"] : [])
+        app.launchArguments += (accountRequired ? ["--account-required-modifier"] : [])
+        app.launchArguments += (verifyAccountDetails ? ["--verify-required-details"] : [])
+        app.launchArguments += (noName ? ["--no-name"] : [])
+        app.launchArguments += flags
         app.launch()
 
         let testApp = TestApp(app: app)
diff --git a/Tests/UITests/UITests.xcodeproj/project.pbxproj b/Tests/UITests/UITests.xcodeproj/project.pbxproj
index c03fd30c..f43e6747 100644
--- a/Tests/UITests/UITests.xcodeproj/project.pbxproj
+++ b/Tests/UITests/UITests.xcodeproj/project.pbxproj
@@ -54,6 +54,7 @@
 		2F6D139228F5F384007C25D6 /* TestApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TestApp.app; sourceTree = BUILT_PRODUCTS_DIR; };
 		2F6D139928F5F386007C25D6 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
 		2F6D13AC28F5F386007C25D6 /* TestAppUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = TestAppUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
+		2FA43E8F2AE022F1009B1B2C /* TestApp.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = TestApp.entitlements; sourceTree = "<group>"; };
 		2FA7382B290ADFAA007ACEB9 /* TestApp.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestApp.swift; sourceTree = "<group>"; };
 		2FAD38BE2A455F7D00E79ED1 /* SpeziAccount */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = SpeziAccount; path = ../..; sourceTree = "<group>"; };
 		2FE750C92A8720CE00723EAE /* TestApp.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = TestApp.xctestplan; sourceTree = "<group>"; };
@@ -132,6 +133,7 @@
 		2F6D139428F5F384007C25D6 /* TestApp */ = {
 			isa = PBXGroup;
 			children = (
+				2FA43E8F2AE022F1009B1B2C /* TestApp.entitlements */,
 				2F027C9629D6C63300234098 /* AccountTests */,
 				A9EE7D292A3359E800C2B9A9 /* Features.swift */,
 				2FA7382B290ADFAA007ACEB9 /* TestApp.swift */,
@@ -446,6 +448,7 @@
 			buildSettings = {
 				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
 				ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+				CODE_SIGN_ENTITLEMENTS = TestApp/TestApp.entitlements;
 				CODE_SIGN_STYLE = Automatic;
 				CURRENT_PROJECT_VERSION = 1;
 				DEVELOPMENT_ASSET_PATHS = "";
@@ -477,6 +480,7 @@
 			buildSettings = {
 				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
 				ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+				CODE_SIGN_ENTITLEMENTS = TestApp/TestApp.entitlements;
 				CODE_SIGN_STYLE = Automatic;
 				CURRENT_PROJECT_VERSION = 1;
 				DEVELOPMENT_ASSET_PATHS = "";
diff --git a/Tests/UITests/UITests.xcodeproj/xcshareddata/xcschemes/TestApp.xcscheme b/Tests/UITests/UITests.xcodeproj/xcshareddata/xcschemes/TestApp.xcscheme
index 653e7f3f..a8cacf49 100644
--- a/Tests/UITests/UITests.xcodeproj/xcshareddata/xcschemes/TestApp.xcscheme
+++ b/Tests/UITests/UITests.xcodeproj/xcshareddata/xcschemes/TestApp.xcscheme
@@ -62,6 +62,14 @@
             argument = "mail"
             isEnabled = "YES">
          </CommandLineArgument>
+         <CommandLineArgument
+            argument = "--configuration-type"
+            isEnabled = "NO">
+         </CommandLineArgument>
+         <CommandLineArgument
+            argument = "allRequiredWithBio"
+            isEnabled = "NO">
+         </CommandLineArgument>
          <CommandLineArgument
             argument = "--default-credentials"
             isEnabled = "NO">