From 22c3e6d298883bd1e401dc3e29c89222ed9139d0 Mon Sep 17 00:00:00 2001 From: Paul Kraft Date: Tue, 12 Nov 2024 15:45:57 -0800 Subject: [PATCH] Fill with more functionality --- .../personalInfo/PersonNameComponents.kt | 11 + .../validation/views/VerifiableTextField.kt | 1 + .../spezi/module/account/account/Account.kt | 2 +- .../account/account/AccountConfiguration.kt | 26 +- .../account/account/ExternalAccountStorage.kt | 5 +- .../AccountServiceConfiguration.kt | 9 + .../compositionLocal/AccountViewType.kt | 14 + .../compositionLocal/PasswordFieldType.kt | 24 + .../SignUpProviderCompliance.kt | 46 +- .../account/mock/InMemoryAccountService.kt | 606 ++++++++---------- .../configuration/FieldValidationRules.kt | 7 + .../configuration/RequiredAccountKeys.kt | 4 +- .../configuration/SupportedAccountKeys.kt | 13 +- .../identityProvider/IdentityProvider.kt | 8 +- .../IdentityProviderConfiguration.kt | 1 + .../account/account/value/AccountKey.kt | 5 + .../value/collections/AccountDetails.kt | 32 +- .../collections/AccountDetailsSerializer.kt | 72 +++ .../value/collections/AccountModifications.kt | 13 +- .../account/value/keys/AccountIdKey.kt | 2 + .../AccountServiceConfigurationDetailsKey.kt | 2 +- .../account/value/keys/DateOfBirthKey.kt | 22 +- .../account/value/keys/DecodingErrorsKey.kt | 12 + .../account/value/keys/EmailAddressKey.kt | 31 +- .../account/value/keys/GenderIdentityKey.kt | 20 +- .../account/account/value/keys/PasswordKey.kt | 48 +- .../account/value/keys/PersonNameKey.kt | 88 ++- .../account/account/value/keys/UserIdKey.kt | 19 +- .../display/GridValidationStateFooter.kt | 16 + .../AccountSetupProviderComposable.kt | 11 + 30 files changed, 786 insertions(+), 384 deletions(-) create mode 100644 modules/account/src/main/kotlin/edu/stanford/spezi/module/account/account/compositionLocal/AccountServiceConfiguration.kt create mode 100644 modules/account/src/main/kotlin/edu/stanford/spezi/module/account/account/compositionLocal/AccountViewType.kt create mode 100644 modules/account/src/main/kotlin/edu/stanford/spezi/module/account/account/compositionLocal/PasswordFieldType.kt create mode 100644 modules/account/src/main/kotlin/edu/stanford/spezi/module/account/account/value/collections/AccountDetailsSerializer.kt create mode 100644 modules/account/src/main/kotlin/edu/stanford/spezi/module/account/account/value/keys/DecodingErrorsKey.kt create mode 100644 modules/account/src/main/kotlin/edu/stanford/spezi/module/account/account/views/display/GridValidationStateFooter.kt diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/PersonNameComponents.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/PersonNameComponents.kt index c87fe5045..258eaa87f 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/PersonNameComponents.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/PersonNameComponents.kt @@ -33,4 +33,15 @@ data class PersonNameComponents( .filter { it.isUpperCase() } } } + + companion object { + operator fun invoke(string: String): PersonNameComponents { + val components = string.split(" ") + // TODO: Vastly improve this implementation! + return PersonNameComponents( + givenName = components.firstOrNull(), + familyName = components.drop(1).joinToString(" ") + ) + } + } } diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/views/VerifiableTextField.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/views/VerifiableTextField.kt index cc9680f91..863c9eb92 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/views/VerifiableTextField.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/views/VerifiableTextField.kt @@ -14,6 +14,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.text.input.TextFieldValue import edu.stanford.spezi.core.design.component.StringResource import edu.stanford.spezi.core.design.theme.SpeziTheme import edu.stanford.spezi.core.design.theme.ThemePreviews diff --git a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/account/Account.kt b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/account/Account.kt index 8a5c15663..00d2435d0 100644 --- a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/account/Account.kt +++ b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/account/Account.kt @@ -13,8 +13,8 @@ import kotlinx.coroutines.runBlocking import javax.inject.Inject class Account( - val configuration: AccountValueConfiguration = AccountValueConfiguration.default, service: AccountService, + val configuration: AccountValueConfiguration = AccountValueConfiguration.default, details: AccountDetails? = null, ) { val logger by speziLogger() diff --git a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/account/AccountConfiguration.kt b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/account/AccountConfiguration.kt index da17acea2..181a7b78f 100644 --- a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/account/AccountConfiguration.kt +++ b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/account/AccountConfiguration.kt @@ -6,21 +6,29 @@ import edu.stanford.spezi.module.account.account.service.configuration.Supported import edu.stanford.spezi.module.account.account.service.configuration.supportedAccountKeys import edu.stanford.spezi.module.account.account.service.configuration.unsupportedAccountKeys import edu.stanford.spezi.module.account.account.value.AccountKeys +import edu.stanford.spezi.module.account.account.value.collections.AccountDetails import edu.stanford.spezi.module.account.account.value.configuration.AccountValueConfiguration import edu.stanford.spezi.module.account.account.value.keys.accountId import javax.inject.Inject import kotlin.system.exitProcess -class AccountConfiguration { - private val logger by speziLogger() - - @Inject lateinit var account: Account +interface Standard - @Inject internal lateinit var externalStorage: ExternalAccountStorage - - @Inject internal lateinit var accountService: Service +// TODO: Expose those properties!!!! +class AccountConfiguration( + private val accountService: Service, + private val storageProvider: AccountStorageProvider? = null, + configuration: AccountValueConfiguration = AccountValueConfiguration.default, + defaultActiveDetails: AccountDetails? = null, +) { + private val logger by speziLogger() - @Inject internal lateinit var storageProvider: List // TODO: This is never going to work + val account = Account( + accountService, + configuration, + defaultActiveDetails + ) + val externalStorage = ExternalAccountStorage(storageProvider) @Inject internal lateinit var standard: Standard @@ -44,7 +52,7 @@ class AccountConfiguration { if (unmappedAccountKeys.isEmpty()) return // we are fine, nothing unsupported - storageProvider.firstOrNull()?.let { + storageProvider?.let { logger.w { """ The storage provider $it is used to store the following account values that diff --git a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/account/ExternalAccountStorage.kt b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/account/ExternalAccountStorage.kt index beec4fb7d..c4d4f603a 100644 --- a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/account/ExternalAccountStorage.kt +++ b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/account/ExternalAccountStorage.kt @@ -11,14 +11,15 @@ import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.onCompletion import java.util.UUID -class ExternalAccountStorage { +class ExternalAccountStorage internal constructor( + private var storageProvider: AccountStorageProvider? +) { data class ExternallyStoredDetails internal constructor( val accountId: String, val details: AccountDetails, ) private var subscriptions = mutableMapOf>() - private var storageProvider: AccountStorageProvider? = null val updatedDetails: Flow get() = UUID().let { id -> diff --git a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/account/compositionLocal/AccountServiceConfiguration.kt b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/account/compositionLocal/AccountServiceConfiguration.kt new file mode 100644 index 000000000..f6363158a --- /dev/null +++ b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/account/compositionLocal/AccountServiceConfiguration.kt @@ -0,0 +1,9 @@ +package edu.stanford.spezi.module.account.account.compositionLocal + +import androidx.compose.runtime.compositionLocalOf +import edu.stanford.spezi.module.account.account.service.configuration.AccountServiceConfiguration +import edu.stanford.spezi.module.account.account.service.configuration.SupportedAccountKeys + +val LocalAccountServiceConfiguration = compositionLocalOf { + AccountServiceConfiguration(SupportedAccountKeys.Arbitrary) +} diff --git a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/account/compositionLocal/AccountViewType.kt b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/account/compositionLocal/AccountViewType.kt new file mode 100644 index 000000000..d15548083 --- /dev/null +++ b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/account/compositionLocal/AccountViewType.kt @@ -0,0 +1,14 @@ +package edu.stanford.spezi.module.account.account.compositionLocal + +import androidx.compose.runtime.compositionLocalOf + +sealed interface AccountViewType { + data object Signup : AccountViewType + data class Overview(val mode: OverviewEntryMode) : AccountViewType + + enum class OverviewEntryMode { + NEW, EXISTING, DISPLAY + } +} + +val LocalAccountViewType = compositionLocalOf { null } diff --git a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/account/compositionLocal/PasswordFieldType.kt b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/account/compositionLocal/PasswordFieldType.kt new file mode 100644 index 000000000..f96b3c804 --- /dev/null +++ b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/account/compositionLocal/PasswordFieldType.kt @@ -0,0 +1,24 @@ +package edu.stanford.spezi.module.account.account.compositionLocal + +import androidx.compose.runtime.compositionLocalOf +import edu.stanford.spezi.core.design.component.StringResource +import edu.stanford.spezi.module.account.account.value.AccountKeys +import edu.stanford.spezi.module.account.account.value.keys.password + +enum class PasswordFieldType { + PASSWORD, NEW, REPEAT; + + val text: StringResource get() = when (this) { + PASSWORD -> AccountKeys.password.name + NEW -> StringResource("NEW_PASSWORD") + REPEAT -> StringResource("REPEAT_PASSWORD") + } + + val prompt: StringResource get() = when (this) { + PASSWORD -> AccountKeys.password.name + NEW -> StringResource("NEW_PASSWORD_PROMPT") + REPEAT -> StringResource("REPEAT_PASSWORD_PROMPT") + } +} + +val LocalPasswordFieldType = compositionLocalOf { PasswordFieldType.PASSWORD } diff --git a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/account/compositionLocal/SignUpProviderCompliance.kt b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/account/compositionLocal/SignUpProviderCompliance.kt index a4903698e..eb1afe68f 100644 --- a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/account/compositionLocal/SignUpProviderCompliance.kt +++ b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/account/compositionLocal/SignUpProviderCompliance.kt @@ -1,5 +1,10 @@ package edu.stanford.spezi.module.account.account.compositionLocal +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.compositionLocalOf +import androidx.compose.runtime.remember import edu.stanford.spezi.module.account.account.value.AccountKey import java.util.Date @@ -21,4 +26,43 @@ data class SignUpProviderCompliance internal constructor( } } -// TODO: Find equivalent to SwiftUI preferences and implement inject functionality +private data class SignupProviderComplianceReader( + var entry: Entry? = null, +) { + data class Entry( + val compliance: SignUpProviderCompliance, + val date: Date = Date(), + ) +} + +private val LocalSignupProviderComplianceReaders = compositionLocalOf { emptyList() } + +@Composable +fun ReportSignupProviderCompliance(compliance: SignUpProviderCompliance?) { + compliance?.let { + val newEntry = SignupProviderComplianceReader.Entry(it) + LocalSignupProviderComplianceReaders.current.forEach { reader -> + val oldEntry = reader.entry + if (oldEntry == null || oldEntry.date > newEntry.date) { + reader.entry = newEntry + } + } + } +} + +@Composable +internal fun ReceiveSignupProviderCompliance( + action: (SignUpProviderCompliance?) -> Unit, + content: @Composable () -> Unit +) { + val newReader = remember { SignupProviderComplianceReader() } + val existingReaders = LocalSignupProviderComplianceReaders.current + CompositionLocalProvider( + LocalSignupProviderComplianceReaders provides (existingReaders + newReader) + ) { + content() + } + LaunchedEffect(newReader.entry) { + action(newReader.entry?.compliance) + } +} diff --git a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/account/mock/InMemoryAccountService.kt b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/account/mock/InMemoryAccountService.kt index 9d6fc19a0..30e46e99d 100644 --- a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/account/mock/InMemoryAccountService.kt +++ b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/account/mock/InMemoryAccountService.kt @@ -1,19 +1,33 @@ package edu.stanford.spezi.module.account.account.mock -/* -import android.accounts.Account +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import edu.stanford.spezi.core.design.component.StringResource +import edu.stanford.spezi.core.design.views.personalInfo.PersonNameComponents +import edu.stanford.spezi.core.design.views.views.views.button.SuspendButton import edu.stanford.spezi.core.logging.speziLogger import edu.stanford.spezi.core.utils.UUID +import edu.stanford.spezi.module.account.account.Account import edu.stanford.spezi.module.account.account.AccountNotifications import edu.stanford.spezi.module.account.account.ExternalAccountStorage +import edu.stanford.spezi.module.account.account.compositionLocal.LocalAccount import edu.stanford.spezi.module.account.account.model.GenderIdentity import edu.stanford.spezi.module.account.account.service.AccountService import edu.stanford.spezi.module.account.account.service.configuration.AccountServiceConfiguration +import edu.stanford.spezi.module.account.account.service.configuration.AccountServiceConfigurationPair +import edu.stanford.spezi.module.account.account.service.configuration.RequiredAccountKeys +import edu.stanford.spezi.module.account.account.service.configuration.SupportedAccountKeys +import edu.stanford.spezi.module.account.account.service.configuration.UserIdConfiguration import edu.stanford.spezi.module.account.account.service.identityProvider.AccountSetupSection +import edu.stanford.spezi.module.account.account.service.identityProvider.ComposableModifier import edu.stanford.spezi.module.account.account.service.identityProvider.IdentityProvider import edu.stanford.spezi.module.account.account.service.identityProvider.SecurityRelatedModifier import edu.stanford.spezi.module.account.account.value.AccountKeys import edu.stanford.spezi.module.account.account.value.collections.AccountDetails +import edu.stanford.spezi.module.account.account.value.collections.AccountModifications import edu.stanford.spezi.module.account.account.value.keys.accountId import edu.stanford.spezi.module.account.account.value.keys.dateOfBirth import edu.stanford.spezi.module.account.account.value.keys.genderIdentity @@ -22,415 +36,365 @@ import edu.stanford.spezi.module.account.account.value.keys.isNewUser import edu.stanford.spezi.module.account.account.value.keys.name import edu.stanford.spezi.module.account.account.value.keys.password import edu.stanford.spezi.module.account.account.value.keys.userId -import kotlinx.coroutines.flow.launchIn +import edu.stanford.spezi.module.account.account.views.setup.provider.AccountServiceButton +import edu.stanford.spezi.module.account.account.views.setup.provider.AccountSetupProviderComposable +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking import java.util.Date +import java.util.EnumSet import java.util.UUID import javax.inject.Inject +import kotlin.coroutines.Continuation +import kotlin.coroutines.cancellation.CancellationException +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException +import kotlin.coroutines.suspendCoroutine +import kotlin.time.Duration.Companion.milliseconds + +@Composable +private fun MockUserIdPasswordEmbeddedComposable( + service: InMemoryAccountService +) { + AccountSetupProviderComposable( + login = { + service.login(it.userId, it.password) + }, + signup = { + service.signUp(it) + }, + resetPassword = { + service.resetPassword(it) + } + ) +} + +@Composable +private fun AnonymousSignupButton(service: InMemoryAccountService) { + // TODO: Coloring + AccountServiceButton(action = { + service.signInAnonymously() + }) { + Text("Stanford SUNet") + } +} + +@Composable +private fun MockSignInWithGoogleButton() { + val account = LocalAccount.current -class InMemoryAccountService : AccountService { + // TODO: Missing component in general, I think +} + +data class MockSecurityAlert(val service: InMemoryAccountService) : ComposableModifier { + @Composable + override fun Body(content: @Composable () -> Unit) { + if (service.state.presentingSecurityAlert.value) { + AlertDialog( + onDismissRequest = { + service.state.presentingSecurityAlert.value = false + }, + title = { + Text("Security Alert") + }, + confirmButton = { + SuspendButton(StringResource("Continue")) { + service.state.securityContinuation?.resume(Unit) + } + }, + dismissButton = { + SuspendButton(StringResource("Cancel")) { + service.state.securityContinuation?.resumeWithException(CancellationException()) + } + } + ) + } + } +} + +class InMemoryAccountService( + type: UserIdConfiguration = UserIdConfiguration.emailAddress, + configured: EnumSet = EnumSet.allOf(ConfiguredIdentityProvider::class.java) +) : AccountService { companion object { - private val supportedKeys = listOf( - AccountKeys::accountId, - AccountKeys::userId, - AccountKeys::password, - AccountKeys::name, - AccountKeys::genderIdentity, - AccountKeys::dateOfBirth + val supportedKeys = listOf( + AccountKeys.accountId, + AccountKeys.userId, + AccountKeys.password, + AccountKeys.name, + AccountKeys.genderIdentity, + AccountKeys.dateOfBirth, ) } - private val logger by speziLogger() + val logger by speziLogger() @Inject private lateinit var account: Account + @Inject private lateinit var notifications: AccountNotifications + @Inject private lateinit var externalStorage: ExternalAccountStorage - private val loginView by IdentityProvider(section = AccountSetupSection.primary, content = {}) - private val testButton2 by IdentityProvider(content = {}) - private val signInWithApple by IdentityProvider(section = AccountSetupSection.singleSignOn, content = {}) + private val loginViewDelegate = IdentityProvider(section = AccountSetupSection.primary) { + MockUserIdPasswordEmbeddedComposable(this) + } + private val loginView by loginViewDelegate - private val securityAlert by SecurityRelatedModifier(MockSecurityAlert()) + private val testButton2Delegate = IdentityProvider { AnonymousSignupButton(this) } + private val testButton2 by testButton2Delegate - val configuration: AccountServiceConfiguration + private val signInWithGoogleDelegate = IdentityProvider(section = AccountSetupSection.singleSignOn) { + MockSignInWithGoogleButton() + } + private val signInWithGoogle by signInWithGoogleDelegate + + private val securityAlertDelegate = SecurityRelatedModifier { MockSecurityAlert(this) } + private val securityAlert by securityAlertDelegate + + override val configuration = AccountServiceConfiguration( + SupportedAccountKeys.Exactly(supportedKeys), + listOf( + AccountServiceConfigurationPair(UserIdConfiguration.key, type), + AccountServiceConfigurationPair( + RequiredAccountKeys.key, + RequiredAccountKeys(listOf(AccountKeys.userId, AccountKeys.password)) + ) + ), + ) val state = State() - private var userIdToAccountId = mutableMapOf() private var registeredUsers = mutableMapOf() - data class State(val id: String = "") // TODO - - data class UserStorage( - val accountId: UUID, - var userId: String?, - var password: String?, - var name: String? = null, // TODO: PersonNameComponents - var genderIdentity: GenderIdentity? = null, - var dateOfBirth: Date? = null, - ) + enum class ConfiguredIdentityProvider { + UserIdPassword, Custom, SignInWithGoogle + } - constructor(type: UserIdConfiguration) - - /// Create a new userId- and password-based account service. - /// - Parameters: - /// - type: The ``UserIdType`` to use for the account service. - /// - configured: The set of identity providers to enable. - public init(_ type: UserIdConfiguration = .emailAddress, configure configured: ConfiguredIdentityProvider = .all) { - self.configuration = AccountServiceConfiguration(supportedKeys: .exactly(Self.supportedKeys)) { - type - RequiredAccountKeys { - \.userId - \.password - } - } + init { - if !configured.contains(.userIdPassword) { - $loginView.isEnabled = false + if (!configured.contains(ConfiguredIdentityProvider.UserIdPassword)) { + loginViewDelegate.isEnabled = false } - if !configured.contains(.customIdentityProvider) { - $testButton2.isEnabled = false + if (!configured.contains(ConfiguredIdentityProvider.Custom)) { + testButton2Delegate.isEnabled = false } - if !configured.contains(.signInWithApple) { - $signInWithApple.isEnabled = false + if (!configured.contains(ConfiguredIdentityProvider.SignInWithGoogle)) { + signInWithGoogleDelegate.isEnabled = false } } init { - val detailsFlow = externalStorage.updatedDetails - runBlocking { - launch { - detailsFlow - .onEach { details -> - runCatching { - val accountId = UUID(details.accountId) - val storage = registeredUsers[accountId] ?: run { return@runCatching } - - access.waitCheckingCancellation() - var details = _buildUser(storage, isNew = false) - account.supplyUserDetails(details) - access.signal() - } - } - .launchIn(this) + val subscription = externalStorage.updatedDetails + GlobalScope.launch { // TODO: Figure out how to do weak reference logic here + subscription.onEach { updatedDetails -> + val accountId = UUID(updatedDetails.accountId) + registeredUsers[accountId]?.let { storage -> + val details = _buildUser(storage, isNew = false) + details.addContentsOf(updatedDetails.details) + account.supplyUserDetails(details) + } } } } fun signInAnonymously() { - val id = UUID() + val accountId = UUID() val details = AccountDetails() - details.accountId = id.toString() - details.isAnonymous = true - details.isNewUser = true - - registeredUsers[id] = UserStorage(id, null, null) - account.supplyUserDetails(details) - } - - public func signInAnonymously() { - let id = UUID() - - var details = AccountDetails() - details.accountId = id.uuidString + details.accountId = accountId.toString() details.isAnonymous = true details.isNewUser = true - registeredUsers[id] = UserStorage(accountId: id, userId: nil, password: nil) + registeredUsers[accountId] = UserStorage(accountId = accountId, userId = null, password = null) account.supplyUserDetails(details) } - public func login(userId: String, password: String) async throws { - logger.debug("Trying to login \(userId) with password \(password)") - try await Task.sleep(for: .milliseconds(500)) + suspend fun login(userId: String, password: String) { + logger.w { "Trying to login $userId with password $password" } + delay(500.milliseconds) - guard let accountId = userIdToAccountId[userId], - let user = registeredUsers[accountId], - user.password == password else { - throw AccountError.wrongCredentials - } + val user = userIdToAccountId[userId]?.let { registeredUsers[it] } + if (user == null || user.password != password) error("WRONG_CREDENTIALS") - try await loadUser(user) - } + loadUser(user) + } - public func signUp(with signupDetails: AccountDetails) async throws { - logger.debug("Signing up user account \(signupDetails.userId)") - try await Task.sleep(for: .milliseconds(500)) + suspend fun signUp(signUpDetails: AccountDetails) { + logger.w { "Signing up user account ${signUpDetails.userId}" } + delay(500.milliseconds) - guard userIdToAccountId[signupDetails.userId] == nil else { - throw AccountError.credentialsTaken - } + if (userIdToAccountId[signUpDetails.userId] != null) { + error("Credentials taken") + } - guard let password = signupDetails.password else { - throw AccountError.internalError - } + val password = signUpDetails.password ?: error("Internal error") + + val storage: UserStorage + val details = account.details + val registered = details?.accountId?.let { registeredUsers[UUID(it)] } + if (details != null && registered != null) { + if (details.isAnonymous) error("Internal error") + + // do account linking for anonymous accounts!´ + storage = registered + storage.userId = signUpDetails.userId + storage.password = password + signUpDetails.name?.let { storage.name = it } + storage.genderIdentity = signUpDetails.genderIdentity + storage.dateOfBirth = signUpDetails.dateOfBirth + } else { + storage = UserStorage( + userId = signUpDetails.userId, + password = password, + name = signUpDetails.name, + genderIdentity = signUpDetails.genderIdentity, + dateOfBirth = signUpDetails.dateOfBirth + ) + } - var storage: UserStorage - if let details = account.details, - let registered = registeredUsers[details.accountId.assumeUUID] { - guard details.isAnonymous else { - throw AccountError.internalError - } + userIdToAccountId[signUpDetails.userId] = storage.accountId + registeredUsers[storage.accountId] = storage - // do account linking for anonymous accounts!´ - storage = registered - storage.userId = signupDetails.userId - storage.password = password - if let name = signupDetails.name { - storage.name = name - } - if let genderIdentity = signupDetails.genderIdentity { - storage.genderIdentity = genderIdentity - } - if let dateOfBirth = signupDetails.dateOfBirth { - storage.dateOfBirth = dateOfBirth - } - } else { - storage = UserStorage( - userId: signupDetails.userId, - password: password, - name: signupDetails.name, - genderIdentity: signupDetails.genderIdentity, - dateOfBirth: signupDetails.dateOfBirth - ) - } + val externallyStored = signUpDetails.copy() + for (key in supportedKeys) { + externallyStored.remove(key) + } + if (!externallyStored.isEmpty()) { + externalStorage.requestExternalStorage(storage.accountId.toString(), externallyStored) + } + loadUser(storage, isNew = true) + } - userIdToAccountId[signupDetails.userId] = storage.accountId - registeredUsers[storage.accountId] = storage + suspend fun resetPassword(userId: String) { + logger.w { "Sending password reset e-mail for $userId" } + delay(500.milliseconds) + } - var externallyStored = signupDetails - externallyStored.removeAll(Self.supportedKeys) - if !externallyStored.isEmpty { - let externalStorage = externalStorage - try await externalStorage.requestExternalStorage(of: externallyStored, for: storage.accountId.uuidString) - } + override suspend fun logout() { + logger.w { "Logging out user" } + delay(500.milliseconds) + account.removeUserDetails() + } - try await loadUser(storage, isNew: true) - } + override suspend fun delete() { + val details = account.details ?: return - public func resetPassword(userId: String) async throws { - logger.debug("Sending password reset e-mail for \(userId)") - try await Task.sleep(for: .milliseconds(500)) - } + logger.w { "Deleting user account for ${details.userId}" } + delay(500.milliseconds) - public func logout() async throws { - logger.debug("Logging out user") - try await Task.sleep(for: .milliseconds(500)) - account.removeUserDetails() + suspendCoroutine { + state.securityContinuation = it + state.presentingSecurityAlert.value = true } - public func delete() async throws { - guard let details = account.details else { - return - } + notifications.reportEvent(AccountNotifications.Event.DeletingAccount(details.accountId)) - logger.debug("Deleting user account for \(details.userId)") - try await Task.sleep(for: .milliseconds(500)) + registeredUsers.remove(UUID(details.accountId)) + userIdToAccountId.remove(details.userId) - try await withCheckedThrowingContinuation { continuation in - state.presentingSecurityAlert = true - state.securityContinuation = continuation - } + account.removeUserDetails() + } - let notifications = notifications - try await notifications.reportEvent(.deletingAccount(details.accountId)) + override suspend fun updateAccountDetails(modifications: AccountModifications) { + val details = account.details ?: error("Internal Error") + val accountId = UUID(details.accountId) - registeredUsers.removeValue(forKey: details.accountId.assumeUUID) - userIdToAccountId.removeValue(forKey: details.userId) + var storage = registeredUsers[accountId] ?: error("Internal Error") - account.removeUserDetails() - } + logger.w { "Updating user details for ${details.userId}: $modifications" } + delay(500.milliseconds) - @MainActor - public func updateAccountDetails(_ modifications: AccountModifications) async throws { - guard let details = account.details else { - throw AccountError.internalError + if (modifications.modifiedDetails.contains(AccountKeys.userId) || + modifications.modifiedDetails.contains(AccountKeys.password)) { + suspendCoroutine { + state.securityContinuation = it + state.presentingSecurityAlert.value = true + } } - guard let accountId = UUID(uuidString: details.accountId) else { - preconditionFailure("Invalid accountId format \(details.accountId)") - } + storage.update(modifications) + registeredUsers[accountId] = storage - guard var storage = registeredUsers[accountId] else { - throw AccountError.internalError + val externalModifications = modifications + externalModifications.removeModifications(supportedKeys) + if (!externalModifications.isEmpty()) { + externalStorage.updateExternalStorage(accountId.toString(), externalModifications) } + loadUser(storage) + } - logger.debug("Updating user details for \(details.userId): \(String(describing: modifications))") - try await Task.sleep(for: .milliseconds(500)) - - if modifications.modifiedDetails.contains(AccountKeys.userId) || modifications.modifiedDetails.contains(AccountKeys.password) { - try await withCheckedThrowingContinuation { continuation in - state.presentingSecurityAlert = true - state.securityContinuation = continuation - } - } - - storage.update(modifications) - registeredUsers[accountId] = storage - - var externalModifications = modifications - externalModifications.removeModifications(for: Self.supportedKeys) - if !externalModifications.isEmpty { - let externalStorage = externalStorage - try await externalStorage.updateExternalStorage(with: externalModifications, for: accountId.uuidString) - } - try await loadUser(storage) - } + private suspend fun loadUser(user: UserStorage, isNew: Boolean = false) { + val details = _buildUser(user, isNew = isNew) - - private func loadUser(_ user: UserStorage, isNew: Bool = false) async throws { - try await access.waitCheckingCancellation() - defer { - access.signal() - } - var details = _buildUser(from: user, isNew: isNew) - - var unsupportedKeys = account.configuration.keys - unsupportedKeys.removeAll(Self.supportedKeys) - if !unsupportedKeys.isEmpty { - let externalStorage = externalStorage - let externallyStored = await externalStorage.retrieveExternalStorage(for: user.accountId.uuidString, unsupportedKeys) - details.add(contentsOf: externallyStored) - } - - account.supplyUserDetails(details) + val unsupportedKeys = account.configuration.keys.filter { + !supportedKeys.contains(it) + } + if (unsupportedKeys.isNotEmpty()) { + val externalStorage = externalStorage + val externallyStored = externalStorage.retrieveExternalStorage(user.accountId.toString(), unsupportedKeys) + details.addContentsOf(externallyStored) } - private func _buildUser(from storage: UserStorage, isNew: Bool) -> AccountDetails { - var details = AccountDetails() - details.accountId = storage.accountId.uuidString + account.supplyUserDetails(details) + } + + private fun _buildUser(storage: UserStorage, isNew: Boolean): AccountDetails { + val details = AccountDetails() + details.accountId = storage.accountId.toString() details.name = storage.name - details.genderIdentity = storage.genderIdentity - details.dateOfBirth = storage.dateOfBirth + storage.genderIdentity?.let { details.genderIdentity = it } + storage.dateOfBirth?.let { details.dateOfBirth = it } details.isNewUser = isNew - if let userId = storage.userId { - details.userId = userId + storage.userId?.let { + details.userId = it } - if storage.password == nil { + if (storage.password == null) { details.isAnonymous = true } return details } -} - - -extension InMemoryAccountService { - public enum AccountError: LocalizedError { - case credentialsTaken - case wrongCredentials - case internalError - case cancelled - - - public var errorDescription: String? { - switch self { - case .credentialsTaken: - return "User Identifier is already taken" - case .wrongCredentials: - return "Credentials do not match" - case .internalError: - return "Internal Error" - case .cancelled: - return "Cancelled" - } - } - - public var failureReason: String? { - errorDescription - } - public var recoverySuggestion: String? { - switch self { - case .credentialsTaken: - return "Please provide a different user identifier." - case .wrongCredentials: - return "Please ensure that the entered credentials are correct." - case .internalError: - return "Something went wrong." - case .cancelled: - return "The user cancelled the operation." - } - } - } + internal data class UserStorage( + val accountId: UUID = UUID(), + var userId: String? = null, + var password: String? = null, + var name: PersonNameComponents? = null, + var genderIdentity: GenderIdentity? = null, + var dateOfBirth: Date? = null + ) - public struct ConfiguredIdentityProvider: OptionSet, Sendable { - public static let userIdPassword = ConfiguredIdentityProvider(rawValue: 1 << 0) - public static let customIdentityProvider = ConfiguredIdentityProvider(rawValue: 1 << 1) - public static let signInWithApple = ConfiguredIdentityProvider(rawValue: 1 << 2) - public static let all: ConfiguredIdentityProvider = [.userIdPassword, .customIdentityProvider, .signInWithApple] + data class State( + var presentingSecurityAlert: MutableState = mutableStateOf(false), + var securityContinuation: Continuation? = null, + ) +} - public let rawValue: UInt8 +private fun InMemoryAccountService.UserStorage.update(modifications: AccountModifications) { + val modifiedDetails = modifications.modifiedDetails + val removedKeys = modifications.removedAccountDetails - public init(rawValue: UInt8) { - self.rawValue = rawValue - } + if (modifiedDetails.contains(AccountKeys.userId)) { + userId = modifiedDetails.userId } + password = modifiedDetails.password + name = modifiedDetails.name + genderIdentity = modifiedDetails.genderIdentity + dateOfBirth = modifiedDetails.dateOfBirth - @Observable - @MainActor - final class State { - var presentingSecurityAlert = false - var securityContinuation: CheckedContinuation? - } + // user Id cannot be removed! - fileprivate struct UserStorage { - let accountId: UUID - var userId: String? - var password: String? - var name: PersonNameComponents? - var genderIdentity: GenderIdentity? - var dateOfBirth: Date? - - init( // swiftlint:disable:this function_default_parameter_at_end - accountId: UUID = UUID(), - userId: String?, - password: String?, - name: PersonNameComponents? = nil, - genderIdentity: GenderIdentity? = nil, - dateOfBirth: Date? = nil - ) { - self.accountId = accountId - self.userId = userId - self.password = password - self.name = name - self.genderIdentity = genderIdentity - self.dateOfBirth = dateOfBirth + if (removedKeys.name != null) { + name = null } + if (removedKeys.genderIdentity != null) { + genderIdentity = null } -} - - -extension InMemoryAccountService.UserStorage { - mutating func update(_ modifications: AccountModifications) { - let modifiedDetails = modifications.modifiedDetails - let removedKeys = modifications.removedAccountDetails - - if modifiedDetails.contains(AccountKeys.userId) { - self.userId = modifiedDetails.userId - } - self.password = modifiedDetails.password ?? password - self.name = modifiedDetails.name ?? name - self.genderIdentity = modifiedDetails.genderIdentity ?? genderIdentity - self.dateOfBirth = modifiedDetails.dateOfBirth ?? dateOfBirth - - // user Id cannot be removed! - - if removedKeys.name != nil { - self.name = nil - } - if removedKeys.genderIdentity != nil { - self.genderIdentity = nil - } - if removedKeys.dateOfBirth != nil { - self.dateOfBirth = nil - } + if (removedKeys.dateOfBirth != null) { + dateOfBirth = null } } - */ diff --git a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/account/service/configuration/FieldValidationRules.kt b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/account/service/configuration/FieldValidationRules.kt index 2f1a745d7..70611e967 100644 --- a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/account/service/configuration/FieldValidationRules.kt +++ b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/account/service/configuration/FieldValidationRules.kt @@ -11,6 +11,7 @@ import edu.stanford.spezi.module.account.account.value.AccountKeys import edu.stanford.spezi.module.account.account.value.keys.email import edu.stanford.spezi.module.account.account.value.keys.password import edu.stanford.spezi.module.account.account.value.keys.userId +import kotlin.reflect.KProperty0 data class FieldValidationRules( val key: AccountKey, @@ -56,3 +57,9 @@ private data class FieldValidationRulesKey( } } } + +fun AccountServiceConfiguration.fieldValidationRules(key: AccountKey): List? = + storage[FieldValidationRules.key(key)]?.rules + +fun AccountServiceConfiguration.fieldValidationRules(property: KProperty0>): List? = + storage[FieldValidationRules.key(property.get())]?.rules diff --git a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/account/service/configuration/RequiredAccountKeys.kt b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/account/service/configuration/RequiredAccountKeys.kt index 68e66924d..c8989b58d 100644 --- a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/account/service/configuration/RequiredAccountKeys.kt +++ b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/account/service/configuration/RequiredAccountKeys.kt @@ -5,7 +5,7 @@ import edu.stanford.spezi.module.account.account.value.AccountKeys import edu.stanford.spezi.module.account.account.value.keys.userId data class RequiredAccountKeys( - val keys: Collection>, + internal val keys: List>, ) { companion object { val key = object : DefaultProvidingAccountServiceConfigurationKey { @@ -15,5 +15,5 @@ data class RequiredAccountKeys( } } -val AccountServiceConfiguration.requiredAccountKeys: Collection> +val AccountServiceConfiguration.requiredAccountKeys: List> get() = storage[RequiredAccountKeys.key].keys diff --git a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/account/service/configuration/SupportedAccountKeys.kt b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/account/service/configuration/SupportedAccountKeys.kt index 7b062008b..9cb40d847 100644 --- a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/account/service/configuration/SupportedAccountKeys.kt +++ b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/account/service/configuration/SupportedAccountKeys.kt @@ -2,11 +2,13 @@ package edu.stanford.spezi.module.account.account.service.configuration import edu.stanford.spezi.module.account.account.value.AccountKey import edu.stanford.spezi.module.account.account.value.configuration.AccountKeyConfiguration +import edu.stanford.spezi.module.account.account.value.configuration.AccountKeyRequirement import edu.stanford.spezi.module.account.account.value.configuration.AccountValueConfiguration +import edu.stanford.spezi.module.account.account.value.isRequired sealed interface SupportedAccountKeys { data object Arbitrary : SupportedAccountKeys - data class Exactly(val keys: Collection>) : SupportedAccountKeys + data class Exactly(val keys: List>) : SupportedAccountKeys fun canStore(value: AccountKeyConfiguration<*>): Boolean { return when (this) { @@ -14,9 +16,9 @@ sealed interface SupportedAccountKeys { true } is Exactly -> { - val key = keys.first { it == value } - - TODO("Not implemented yet") + keys.firstOrNull { it === value.key }?.let { key -> + !key.isRequired || value.requirement == AccountKeyRequirement.REQUIRED + } ?: false } } } @@ -27,7 +29,8 @@ sealed interface SupportedAccountKeys { } var AccountServiceConfiguration.supportedAccountKeys: SupportedAccountKeys - get() = this.storage[SupportedAccountKeys.key] ?: error("Figure out how to translate preconditionFailure.") + get() = this.storage[SupportedAccountKeys.key] + ?: error("Reached illegal state where SupportedAccountKeys configuration was never supplied!") set(value) { this.storage[SupportedAccountKeys.key] = value } fun AccountServiceConfiguration.unsupportedAccountKeys(configuration: AccountValueConfiguration): List> { diff --git a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/account/service/identityProvider/IdentityProvider.kt b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/account/service/identityProvider/IdentityProvider.kt index 4a59137f8..ec9b7d856 100644 --- a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/account/service/identityProvider/IdentityProvider.kt +++ b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/account/service/identityProvider/IdentityProvider.kt @@ -12,20 +12,24 @@ data class AccountSetupComponent internal constructor( ) data class IdentityProvider internal constructor( - val content: @Composable () -> Unit, val configuration: IdentityProviderConfiguration, + val content: @Composable () -> Unit, ) { operator fun getValue(thisRef: Any, property: KProperty<*>) = content val component = AccountSetupComponent(configuration = configuration, content = content) + var isEnabled: Boolean + get() = configuration.isEnabled + set(value) { configuration.isEnabled = value } + companion object { operator fun invoke( isEnabled: Boolean = true, section: AccountSetupSection = AccountSetupSection.default, content: @Composable () -> Unit, ): IdentityProvider { - return IdentityProvider(content, IdentityProviderConfiguration(isEnabled, section)) + return IdentityProvider(IdentityProviderConfiguration(isEnabled, section), content) } } } diff --git a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/account/service/identityProvider/IdentityProviderConfiguration.kt b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/account/service/identityProvider/IdentityProviderConfiguration.kt index 76cfc64d5..531bcd409 100644 --- a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/account/service/identityProvider/IdentityProviderConfiguration.kt +++ b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/account/service/identityProvider/IdentityProviderConfiguration.kt @@ -1,5 +1,6 @@ package edu.stanford.spezi.module.account.account.service.identityProvider +// TODO: May need to have mutableStateOf properties, since it is marked observable on iOS class IdentityProviderConfiguration( var isEnabled: Boolean, var section: AccountSetupSection, diff --git a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/account/value/AccountKey.kt b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/account/value/AccountKey.kt index ff2097815..3c4f054e7 100644 --- a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/account/value/AccountKey.kt +++ b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/account/value/AccountKey.kt @@ -10,12 +10,14 @@ import edu.stanford.spezi.core.utils.foundation.knowledgesource.OptionalComputed import edu.stanford.spezi.module.account.account.value.collections.AccountAnchor import edu.stanford.spezi.module.account.account.value.keys.accountId import edu.stanford.spezi.module.account.account.value.keys.userId +import kotlinx.serialization.KSerializer interface AccountKey : KnowledgeSource { val identifier: String val name: StringResource val category: AccountKeyCategory get() = AccountKeyCategory.other val initialValue: InitialValue + val serializer: KSerializer @Composable fun DisplayComposable(value: Value) @@ -24,6 +26,9 @@ interface AccountKey : KnowledgeSource { fun EntryComposable(state: MutableState) } +internal val AccountKey.isRequired: Boolean + get() = this is RequiredAccountKey + internal val AccountKey.isHiddenCredential: Boolean get() = this == AccountKeys.accountId || this == AccountKeys.userId diff --git a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/account/value/collections/AccountDetails.kt b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/account/value/collections/AccountDetails.kt index 8eb8cda01..a6d822ea3 100644 --- a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/account/value/collections/AccountDetails.kt +++ b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/account/value/collections/AccountDetails.kt @@ -1,28 +1,24 @@ package edu.stanford.spezi.module.account.account.value.collections +import edu.stanford.spezi.core.utils.foundation.SharedRepository +import edu.stanford.spezi.core.utils.foundation.knowledgesource.KnowledgeSource import edu.stanford.spezi.module.account.account.value.AccountKey data class AccountDetails( internal val storage: AccountStorage = AccountStorage(), -) { - fun isEmpty(): Boolean = !storage.any() +) : SharedRepository by storage, Iterable, Any>> { + fun isEmpty() = + !storage.any() - val keys: List> + val keys get() = storage.mapNotNull { it.key as? AccountKey<*> }.toList() fun contains(key: AccountKey<*>) = storage.any { it.key === key } - operator fun get(key: AccountKey): Value? = - storage[key] - - operator fun set(key: AccountKey, value: Value?) { - storage[key] = value - } - fun update(modifications: AccountModifications) { - for (entry in modifications.modifiedDetails.storage) { - storage[entry.key] = entry.value + for (entry in modifications.modifiedDetails.keys) { + entry.copy(modifications.modifiedDetails, this) } for (entry in modifications.removedAccountDetails.storage) { @@ -34,13 +30,21 @@ data class AccountDetails( storage[key] = null } - fun addContentsOf(details: AccountDetails, filter: List>, merge: Boolean = false) { + fun removeAll(keys: List>) { + for (key in keys) { + remove(key) + } + } + + fun addContentsOf(details: AccountDetails, filter: List>? = null, merge: Boolean = false) { for (key in details.keys) { - if (filter.contains(key) && (merge || !contains(key))) { + if ((filter?.contains(key) != false) && (merge || !contains(key))) { key.copy(details, this) } } } + + override fun iterator() = storage.iterator() } private fun AccountKey.copy(source: AccountDetails, destination: AccountDetails) { diff --git a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/account/value/collections/AccountDetailsSerializer.kt b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/account/value/collections/AccountDetailsSerializer.kt new file mode 100644 index 000000000..cef662693 --- /dev/null +++ b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/account/value/collections/AccountDetailsSerializer.kt @@ -0,0 +1,72 @@ +package edu.stanford.spezi.module.account.account.value.collections + +import edu.stanford.spezi.module.account.account.value.AccountKey +import edu.stanford.spezi.module.account.account.value.keys.decodingErrors +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.KSerializer +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.buildClassSerialDescriptor +import kotlinx.serialization.encoding.CompositeDecoder +import kotlinx.serialization.encoding.CompositeEncoder +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.encoding.decodeStructure +import kotlinx.serialization.encoding.encodeStructure + +@OptIn(ExperimentalSerializationApi::class) +class AccountDetailsSerializer( + private val identifierMapping: Map>, + private val requireAllKeys: Boolean = false, + private val lazyDecoding: Boolean = false, +) : KSerializer { + override val descriptor: SerialDescriptor = buildClassSerialDescriptor("AccountDetailsSerializer") { + for (identifier in identifierMapping) { + element(identifier.key, identifier.value.serializer.descriptor, isOptional = !requireAllKeys) + } + } + + override fun deserialize(decoder: Decoder): AccountDetails { + val value = AccountDetails() + val decodingErrors = mutableListOf, Throwable>>() + decoder.decodeStructure(descriptor) { + @Suppress("detekt:TooGenericExceptionCaught") + identifierMapping.onEachIndexed { index, identifier -> + try { + identifier.value.deserialize(this, index, value) + } catch (error: Throwable) { + decodingErrors.add(Pair(identifier.value, error)) + if (!lazyDecoding) throw error + } + } + } + value.decodingErrors = decodingErrors + return value + } + + override fun serialize(encoder: Encoder, value: AccountDetails) { + encoder.encodeStructure(descriptor) { + identifierMapping.onEachIndexed { index, identifier -> + identifier.value.serialize(this, index, value) + } + } + } +} + +@OptIn(ExperimentalSerializationApi::class) +private fun AccountKey.deserialize( + decoder: CompositeDecoder, + index: Int, + details: AccountDetails, +) { + details[this] = decoder.decodeNullableSerializableElement(serializer.descriptor, index, serializer) +} + +private fun AccountKey.serialize( + encoder: CompositeEncoder, + index: Int, + details: AccountDetails +) { + details[this]?.let { + encoder.encodeSerializableElement(serializer.descriptor, index, serializer, it) + } +} diff --git a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/account/value/collections/AccountModifications.kt b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/account/value/collections/AccountModifications.kt index 5bde6c914..c1c84963d 100644 --- a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/account/value/collections/AccountModifications.kt +++ b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/account/value/collections/AccountModifications.kt @@ -1,6 +1,17 @@ package edu.stanford.spezi.module.account.account.value.collections +import edu.stanford.spezi.module.account.account.value.AccountKey + data class AccountModifications( val modifiedDetails: AccountDetails, val removedAccountDetails: AccountDetails = AccountDetails(), -) +) { + val removedAccountKeys: List> get() = removedAccountDetails.keys + + fun isEmpty() = modifiedDetails.isEmpty() && removedAccountDetails.isEmpty() + + fun removeModifications(keys: List>) { + modifiedDetails.removeAll(keys) + removedAccountDetails.removeAll(keys) + } +} diff --git a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/account/value/keys/AccountIdKey.kt b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/account/value/keys/AccountIdKey.kt index b031405be..dc2788073 100644 --- a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/account/value/keys/AccountIdKey.kt +++ b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/account/value/keys/AccountIdKey.kt @@ -9,12 +9,14 @@ import edu.stanford.spezi.module.account.account.value.AccountKeys import edu.stanford.spezi.module.account.account.value.InitialValue import edu.stanford.spezi.module.account.account.value.RequiredAccountKey import edu.stanford.spezi.module.account.account.value.collections.AccountDetails +import kotlinx.serialization.builtins.serializer private object AccountIdKey : RequiredAccountKey { override val identifier = "id" override val name = StringResource("ACCOUNT_ID") override val category = AccountKeyCategory.credentials override val initialValue: InitialValue = InitialValue.Empty("") + override val serializer = String.serializer() @Composable override fun DisplayComposable(value: String) { diff --git a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/account/value/keys/AccountServiceConfigurationDetailsKey.kt b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/account/value/keys/AccountServiceConfigurationDetailsKey.kt index 0dad4ae6f..109846cce 100644 --- a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/account/value/keys/AccountServiceConfigurationDetailsKey.kt +++ b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/account/value/keys/AccountServiceConfigurationDetailsKey.kt @@ -8,7 +8,7 @@ import edu.stanford.spezi.module.account.account.service.configuration.userIdCon import edu.stanford.spezi.module.account.account.value.collections.AccountAnchor import edu.stanford.spezi.module.account.account.value.collections.AccountDetails -private object AccountServiceConfigurationDetailsKey : DefaultProvidingKnowledgeSource { +internal object AccountServiceConfigurationDetailsKey : DefaultProvidingKnowledgeSource { override val defaultValue = AccountServiceConfiguration(supportedKeys = SupportedAccountKeys.Exactly(emptyList())) } diff --git a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/account/value/keys/DateOfBirthKey.kt b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/account/value/keys/DateOfBirthKey.kt index 4791543a8..cca1dc413 100644 --- a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/account/value/keys/DateOfBirthKey.kt +++ b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/account/value/keys/DateOfBirthKey.kt @@ -9,6 +9,12 @@ import edu.stanford.spezi.module.account.account.value.AccountKeys import edu.stanford.spezi.module.account.account.value.InitialValue import edu.stanford.spezi.module.account.account.value.collections.AccountDetails import edu.stanford.spezi.module.account.account.value.value +import kotlinx.serialization.KSerializer +import kotlinx.serialization.builtins.serializer +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import java.time.Instant import java.util.Date private object AccountDateOfBirthKey : AccountKey { @@ -16,6 +22,18 @@ private object AccountDateOfBirthKey : AccountKey { override val name = StringResource("UAP_SIGNUP_DATE_OF_BIRTH_TITLE") override val category = AccountKeyCategory.personalDetails override val initialValue: InitialValue = InitialValue.Empty(Date()) + override val serializer = object : KSerializer { + override val descriptor: SerialDescriptor + get() = String.serializer().descriptor + + override fun serialize(encoder: Encoder, value: Date) { + encoder.encodeString(value.toInstant().toString()) + } + + override fun deserialize(decoder: Decoder): Date { + return Date.from(Instant.parse(decoder.decodeString())) + } + } @Composable override fun DisplayComposable(value: Date) { @@ -31,6 +49,6 @@ private object AccountDateOfBirthKey : AccountKey { val AccountKeys.dateOfBirth: AccountKey get() = AccountDateOfBirthKey -var AccountDetails.dateOfBirth: Date - get() = this.storage[AccountKeys.dateOfBirth] ?: AccountKeys.dateOfBirth.initialValue.value +var AccountDetails.dateOfBirth: Date? + get() = this.storage[AccountKeys.dateOfBirth] set(value) { this.storage[AccountKeys.dateOfBirth] = value } diff --git a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/account/value/keys/DecodingErrorsKey.kt b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/account/value/keys/DecodingErrorsKey.kt new file mode 100644 index 000000000..20418d51e --- /dev/null +++ b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/account/value/keys/DecodingErrorsKey.kt @@ -0,0 +1,12 @@ +package edu.stanford.spezi.module.account.account.value.keys + +import edu.stanford.spezi.core.utils.foundation.knowledgesource.KnowledgeSource +import edu.stanford.spezi.module.account.account.value.AccountKey +import edu.stanford.spezi.module.account.account.value.collections.AccountAnchor +import edu.stanford.spezi.module.account.account.value.collections.AccountDetails + +private object DecodingErrorsKey : KnowledgeSource, Throwable>>> + +var AccountDetails.decodingErrors: List, Throwable>>? + get() = this.storage[DecodingErrorsKey] + set(value) { this.storage[DecodingErrorsKey] = value } diff --git a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/account/value/keys/EmailAddressKey.kt b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/account/value/keys/EmailAddressKey.kt index 2ce4db703..fca08f546 100644 --- a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/account/value/keys/EmailAddressKey.kt +++ b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/account/value/keys/EmailAddressKey.kt @@ -3,8 +3,11 @@ package edu.stanford.spezi.module.account.account.value.keys import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import edu.stanford.spezi.core.design.component.StringResource +import edu.stanford.spezi.core.design.views.validation.views.VerifiableTextField import edu.stanford.spezi.core.utils.foundation.SharedRepository import edu.stanford.spezi.core.utils.foundation.knowledgesource.ComputedKnowledgeSourceStoragePolicy +import edu.stanford.spezi.module.account.account.service.configuration.UserIdType +import edu.stanford.spezi.module.account.account.service.configuration.userIdConfiguration import edu.stanford.spezi.module.account.account.value.AccountKey import edu.stanford.spezi.module.account.account.value.AccountKeyCategory import edu.stanford.spezi.module.account.account.value.AccountKeys @@ -12,38 +15,38 @@ import edu.stanford.spezi.module.account.account.value.InitialValue import edu.stanford.spezi.module.account.account.value.OptionalComputedAccountKey import edu.stanford.spezi.module.account.account.value.collections.AccountAnchor import edu.stanford.spezi.module.account.account.value.collections.AccountDetails +import edu.stanford.spezi.module.account.account.views.display.StringDisplay +import kotlinx.serialization.builtins.serializer private object AccountEmailKey : OptionalComputedAccountKey { override val identifier = "email" - override val name = StringResource("UAP_SIGNUP_DATE_OF_BIRTH_TITLE") + override val name = StringResource("USER_ID_EMAIL") override val category = AccountKeyCategory.personalDetails override val storagePolicy: ComputedKnowledgeSourceStoragePolicy get() = ComputedKnowledgeSourceStoragePolicy.AlwaysCompute override val initialValue: InitialValue = InitialValue.Empty("") + override val serializer = String.serializer() @Composable override fun DisplayComposable(value: String) { - TODO("Not yet implemented") + StringDisplay(this, value) } @Composable override fun EntryComposable(state: MutableState) { - TODO("Not yet implemented") + VerifiableTextField(name, state) + // TODO: Set content type, disable field assistants } override fun compute(repository: SharedRepository): String? { - repository[this]?.let { - return it - } ?: TODO(""" - guard let configuration = repository[AccountDetails.AccountServiceConfigurationDetailsKey.self], - case .emailAddress = configuration.userIdConfiguration.idType else { - return nil + return repository[this] ?: run { + val idType = repository[AccountServiceConfigurationDetailsKey].userIdConfiguration.idType + if (idType == UserIdType.EmailAddress) { + repository[AccountKeys.userId] + } else { + null + } } - - // return the userId if it's a email address - return repository[AccountKeys.userId] - """.trimIndent() - ) } } diff --git a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/account/value/keys/GenderIdentityKey.kt b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/account/value/keys/GenderIdentityKey.kt index f73b623d7..e536fb256 100644 --- a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/account/value/keys/GenderIdentityKey.kt +++ b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/account/value/keys/GenderIdentityKey.kt @@ -10,12 +10,28 @@ import edu.stanford.spezi.module.account.account.value.AccountKeys import edu.stanford.spezi.module.account.account.value.InitialValue import edu.stanford.spezi.module.account.account.value.collections.AccountDetails import edu.stanford.spezi.module.account.account.value.value +import kotlinx.serialization.KSerializer +import kotlinx.serialization.builtins.serializer +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder private object AccountGenderIdentityKey : AccountKey { override val identifier = "genderIdentity" override val name = StringResource("GENDER_IDENTITY_TITLE") override val category = AccountKeyCategory.personalDetails override val initialValue: InitialValue = InitialValue.Default(GenderIdentity.PREFER_NOT_TO_STATE) + override val serializer = object : KSerializer { + override val descriptor = String.serializer().descriptor + + override fun serialize(encoder: Encoder, value: GenderIdentity) { + encoder.encodeString(value.name) + } + + override fun deserialize(decoder: Decoder): GenderIdentity { + val string = decoder.decodeString() + return GenderIdentity.entries.first { it.name == string } + } + } @Composable override fun DisplayComposable(value: GenderIdentity) { @@ -31,6 +47,6 @@ private object AccountGenderIdentityKey : AccountKey { val AccountKeys.genderIdentity: AccountKey get() = AccountGenderIdentityKey -var AccountDetails.genderIdentity: GenderIdentity - get() = this.storage[AccountKeys.genderIdentity] ?: AccountKeys.genderIdentity.initialValue.value +var AccountDetails.genderIdentity: GenderIdentity? + get() = this.storage[AccountKeys.genderIdentity] set(value) { this.storage[AccountKeys.genderIdentity] = value } diff --git a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/account/value/keys/PasswordKey.kt b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/account/value/keys/PasswordKey.kt index 320870e1e..e91b0b0de 100644 --- a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/account/value/keys/PasswordKey.kt +++ b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/account/value/keys/PasswordKey.kt @@ -1,13 +1,26 @@ package edu.stanford.spezi.module.account.account.value.keys +import androidx.compose.material3.Text +import androidx.compose.material3.TextField import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState +import androidx.compose.ui.text.input.PasswordVisualTransformation import edu.stanford.spezi.core.design.component.StringResource +import edu.stanford.spezi.core.design.views.validation.configuration.LocalValidationEngine +import edu.stanford.spezi.core.design.views.validation.views.TextFieldType +import edu.stanford.spezi.core.design.views.validation.views.VerifiableTextField +import edu.stanford.spezi.core.design.views.views.layout.DescriptionGridRow +import edu.stanford.spezi.module.account.account.compositionLocal.AccountViewType +import edu.stanford.spezi.module.account.account.compositionLocal.LocalAccountViewType +import edu.stanford.spezi.module.account.account.compositionLocal.LocalPasswordFieldType import edu.stanford.spezi.module.account.account.value.AccountKey import edu.stanford.spezi.module.account.account.value.AccountKeyCategory import edu.stanford.spezi.module.account.account.value.AccountKeys import edu.stanford.spezi.module.account.account.value.InitialValue import edu.stanford.spezi.module.account.account.value.collections.AccountDetails +import edu.stanford.spezi.module.account.account.views.display.GridValidationStateFooter +import edu.stanford.spezi.module.account.account.views.display.StringDisplay +import kotlinx.serialization.builtins.serializer private object AccountPasswordKey : AccountKey { override val identifier = "password" @@ -15,14 +28,45 @@ private object AccountPasswordKey : AccountKey { override val category = AccountKeyCategory.credentials override val initialValue: InitialValue = InitialValue.Empty("") + // TODO: Since password is not supposed to be serialized, + // we could also just throw an error when this is attempted + // or we ignore the serialization step + override val serializer = String.serializer() + @Composable override fun DisplayComposable(value: String) { - TODO("Not yet implemented") + StringDisplay(this, value) } @Composable override fun EntryComposable(state: MutableState) { - TODO("Not yet implemented") + val accountViewType = LocalAccountViewType.current + val fieldType = LocalPasswordFieldType.current + val validation = LocalValidationEngine.current + + when (accountViewType) { + null, AccountViewType.Signup -> { + VerifiableTextField(fieldType.text, state, type = TextFieldType.SECURE) + // TODO: TextContentType, Disable field assistants + } + is AccountViewType.Overview -> { + DescriptionGridRow( + description = { + Text(fieldType.text.text()) + } + ) { + TextField( + state.value, + onValueChange = { state.value = it }, + visualTransformation = PasswordVisualTransformation() + ) + // TODO: TextContentType, Disable field assistants + } + + GridValidationStateFooter(validation?.displayedValidationResults ?: emptyList()) + } + } + } } diff --git a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/account/value/keys/PersonNameKey.kt b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/account/value/keys/PersonNameKey.kt index e383603a3..7662ec9f5 100644 --- a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/account/value/keys/PersonNameKey.kt +++ b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/account/value/keys/PersonNameKey.kt @@ -1,14 +1,38 @@ package edu.stanford.spezi.module.account.account.value.keys +import androidx.compose.foundation.layout.Column +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import edu.stanford.spezi.core.design.component.ListRow import edu.stanford.spezi.core.design.component.StringResource import edu.stanford.spezi.core.design.views.personalInfo.PersonNameComponents +import edu.stanford.spezi.core.design.views.personalInfo.fields.NameFieldRow +import edu.stanford.spezi.core.design.views.validation.Validate +import edu.stanford.spezi.core.design.views.validation.ValidationEngine +import edu.stanford.spezi.core.design.views.validation.ValidationRule +import edu.stanford.spezi.core.design.views.validation.configuration.LocalValidationEngineConfiguration +import edu.stanford.spezi.core.design.views.validation.nonEmpty +import edu.stanford.spezi.core.design.views.validation.state.ReceiveValidation +import edu.stanford.spezi.core.design.views.validation.state.ValidationContext +import edu.stanford.spezi.module.account.account.compositionLocal.LocalAccount +import edu.stanford.spezi.module.account.account.model.acceptAll import edu.stanford.spezi.module.account.account.value.AccountKey import edu.stanford.spezi.module.account.account.value.AccountKeyCategory import edu.stanford.spezi.module.account.account.value.AccountKeys import edu.stanford.spezi.module.account.account.value.InitialValue import edu.stanford.spezi.module.account.account.value.collections.AccountDetails +import edu.stanford.spezi.module.account.account.value.configuration.AccountKeyRequirement +import edu.stanford.spezi.module.account.account.views.display.GridValidationStateFooter +import kotlinx.serialization.KSerializer +import kotlinx.serialization.builtins.serializer +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import java.util.EnumSet private object AccountNameKey : AccountKey { override val identifier = "name" @@ -16,15 +40,75 @@ private object AccountNameKey : AccountKey { override val category = AccountKeyCategory.credentials override val initialValue: InitialValue = InitialValue.Empty( PersonNameComponents()) + override val serializer = object : KSerializer { + override val descriptor = String.serializer().descriptor + + override fun serialize(encoder: Encoder, value: PersonNameComponents) { + encoder.encodeString(value.formatted()) + } + + override fun deserialize(decoder: Decoder): PersonNameComponents { + return PersonNameComponents(decoder.decodeString()) + } + } @Composable override fun DisplayComposable(value: PersonNameComponents) { - TODO("Not yet implemented") + ListRow(name) { + Text(value.formatted()) + } } @Composable override fun EntryComposable(state: MutableState) { - TODO("Not yet implemented") + val account = LocalAccount.current + val nameIsRequired = account?.configuration?.get(AccountKeys.name)?.requirement == AccountKeyRequirement.REQUIRED + val validationRules = if (nameIsRequired) listOf(ValidationRule.nonEmpty) else listOf(ValidationRule.acceptAll) + + val givenNameValidation = remember { mutableStateOf(ValidationContext()) } + val familyNameValidation = remember { mutableStateOf(ValidationContext()) } + + val validationConfiguration = remember { + EnumSet.of(ValidationEngine.ConfigurationOption.CONSIDER_NO_INPUT_AS_VALID) + } + + CompositionLocalProvider(LocalValidationEngineConfiguration provides validationConfiguration) { + Column { + ReceiveValidation(givenNameValidation) { + Validate(state.value.givenName ?: "", rules = validationRules) { + NameFieldRow( + state, + PersonNameComponents::givenName, + description = { + Text(StringResource("UAP_SIGNUP_GIVEN_NAME_TITLE").text()) + }, + ) { + Text(StringResource("UAP_SIGNUP_GIVEN_NAME_PLACEHOLDER").text()) + } + } + } + + GridValidationStateFooter(givenNameValidation.value.allDisplayedValidationResults) + + HorizontalDivider() + + ReceiveValidation(familyNameValidation) { + Validate(state.value.familyName ?: "", rules = validationRules) { + NameFieldRow( + state, + PersonNameComponents::familyName, + description = { + Text(StringResource("UAP_SIGNUP_FAMILY_NAME_TITLE").text()) + }, + ) { + Text(StringResource("UAP_SIGNUP_FAMILY_NAME_PLACEHOLDER").text()) + } + } + } + + GridValidationStateFooter(familyNameValidation.value.allDisplayedValidationResults) + } + } } } diff --git a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/account/value/keys/UserIdKey.kt b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/account/value/keys/UserIdKey.kt index ed539c4ee..de2596063 100644 --- a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/account/value/keys/UserIdKey.kt +++ b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/account/value/keys/UserIdKey.kt @@ -1,10 +1,15 @@ package edu.stanford.spezi.module.account.account.value.keys +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState +import edu.stanford.spezi.core.design.component.ListRow import edu.stanford.spezi.core.design.component.StringResource +import edu.stanford.spezi.core.design.views.validation.views.VerifiableTextField import edu.stanford.spezi.core.utils.foundation.SharedRepository import edu.stanford.spezi.core.utils.foundation.knowledgesource.ComputedKnowledgeSourceStoragePolicy +import edu.stanford.spezi.module.account.account.compositionLocal.LocalAccountServiceConfiguration +import edu.stanford.spezi.module.account.account.service.configuration.userIdConfiguration import edu.stanford.spezi.module.account.account.value.AccountKey import edu.stanford.spezi.module.account.account.value.AccountKeyCategory import edu.stanford.spezi.module.account.account.value.AccountKeys @@ -12,6 +17,7 @@ import edu.stanford.spezi.module.account.account.value.ComputedAccountKey import edu.stanford.spezi.module.account.account.value.InitialValue import edu.stanford.spezi.module.account.account.value.collections.AccountAnchor import edu.stanford.spezi.module.account.account.value.collections.AccountDetails +import kotlinx.serialization.builtins.serializer private object AccountUserIdKey : ComputedAccountKey { override val identifier: String = "userId" @@ -20,15 +26,22 @@ private object AccountUserIdKey : ComputedAccountKey { override val storagePolicy: ComputedKnowledgeSourceStoragePolicy get() = ComputedKnowledgeSourceStoragePolicy.AlwaysCompute override val initialValue: InitialValue = InitialValue.Empty("") + override val serializer = String.serializer() @Composable override fun DisplayComposable(value: String) { - TODO("Not yet implemented") + val configuration = LocalAccountServiceConfiguration.current + ListRow(configuration.userIdConfiguration.idType.stringResource) { + Text(value) + } } @Composable override fun EntryComposable(state: MutableState) { - TODO("Not yet implemented") + val configuration = LocalAccountServiceConfiguration.current + + VerifiableTextField(configuration.userIdConfiguration.idType.stringResource, state) + // TODO: TextContentTupe, KeyboardType, Disable field assistants } override fun compute(repository: SharedRepository): String { @@ -42,5 +55,5 @@ val AccountKeys.userId: ComputedAccountKey get() = AccountUserIdKey var AccountDetails.userId: String - get() = this.storage[AccountKeys.userId] ?: "" + get() = this.storage[AccountKeys.userId] set(value) { this.storage[AccountKeys.userId] = value } diff --git a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/account/views/display/GridValidationStateFooter.kt b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/account/views/display/GridValidationStateFooter.kt new file mode 100644 index 000000000..fb635252b --- /dev/null +++ b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/account/views/display/GridValidationStateFooter.kt @@ -0,0 +1,16 @@ +package edu.stanford.spezi.module.account.account.views.display + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.runtime.Composable +import edu.stanford.spezi.core.design.views.validation.state.FailedValidationResult +import edu.stanford.spezi.core.design.views.validation.views.ValidationResultsComposable + +@Composable +internal fun GridValidationStateFooter(results: List) { + if (results.isNotEmpty()) { + Row(horizontalArrangement = Arrangement.Start) { + ValidationResultsComposable(results) + } + } +} diff --git a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/account/views/setup/provider/AccountSetupProviderComposable.kt b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/account/views/setup/provider/AccountSetupProviderComposable.kt index 27cd31ad2..173069524 100644 --- a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/account/views/setup/provider/AccountSetupProviderComposable.kt +++ b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/account/views/setup/provider/AccountSetupProviderComposable.kt @@ -2,6 +2,17 @@ package edu.stanford.spezi.module.account.account.views.setup.provider import androidx.compose.runtime.Composable import edu.stanford.spezi.module.account.account.model.UserIdPasswordCredential +import edu.stanford.spezi.module.account.account.value.collections.AccountDetails + + +@Composable +fun AccountSetupProviderComposable( + login: suspend (UserIdPasswordCredential) -> Unit, + signup: suspend (AccountDetails) -> Unit, + resetPassword: suspend (String) -> Unit +) { + TODO("Not implemented yet") +} @Composable fun AccountSetupProviderComposable(