diff --git a/app-feature-preview/src/main/java/app/k9mail/feature/preview/navigation/FeatureNavHost.kt b/app-feature-preview/src/main/java/app/k9mail/feature/preview/navigation/FeatureNavHost.kt index 73d1f225444..5f1a99e19c4 100644 --- a/app-feature-preview/src/main/java/app/k9mail/feature/preview/navigation/FeatureNavHost.kt +++ b/app-feature-preview/src/main/java/app/k9mail/feature/preview/navigation/FeatureNavHost.kt @@ -5,7 +5,7 @@ import androidx.compose.ui.Modifier import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import app.k9mail.feature.account.edit.navigation.accountEditRoute -import app.k9mail.feature.account.edit.navigation.navigateToAccountEditConfigIncoming +import app.k9mail.feature.account.edit.navigation.navigateToAccountEditIncomingServerSettings import app.k9mail.feature.account.setup.navigation.accountSetupRoute import app.k9mail.feature.account.setup.navigation.navigateToAccountSetup import app.k9mail.feature.onboarding.navigation.NAVIGATION_ROUTE_ONBOARDING @@ -30,7 +30,7 @@ fun FeatureNavHost( accountSetupRoute( onBack = navController::popBackStack, onFinish = { accountUuid -> - navController.navigateToAccountEditConfigIncoming(accountUuid) + navController.navigateToAccountEditIncomingServerSettings(accountUuid) }, ) accountEditRoute( diff --git a/app-ui-catalog/src/main/java/app/k9mail/ui/catalog/ui/molecule/items/StateItems.kt b/app-ui-catalog/src/main/java/app/k9mail/ui/catalog/ui/molecule/items/StateItems.kt index 82231091d16..c2810f627e9 100644 --- a/app-ui-catalog/src/main/java/app/k9mail/ui/catalog/ui/molecule/items/StateItems.kt +++ b/app-ui-catalog/src/main/java/app/k9mail/ui/catalog/ui/molecule/items/StateItems.kt @@ -26,7 +26,31 @@ fun LazyGridScope.stateItems() { ItemOutlined { ErrorView( title = "Error", + ) + } + } + item { + ItemOutlined { + ErrorView( + title = "Error with message", + message = "Something went wrong", + ) + } + } + item { + ItemOutlined { + ErrorView( + title = "Error with retry", + onRetry = {}, + ) + } + } + item { + ItemOutlined { + ErrorView( + title = "Error with retry and message", message = "Something went wrong", + onRetry = {}, ) } } diff --git a/app/k9mail/src/main/java/com/fsck/k9/account/AccountServerSettingsUpdater.kt b/app/k9mail/src/main/java/com/fsck/k9/account/AccountServerSettingsUpdater.kt index f91f30354c2..8a2b1b62574 100644 --- a/app/k9mail/src/main/java/com/fsck/k9/account/AccountServerSettingsUpdater.kt +++ b/app/k9mail/src/main/java/com/fsck/k9/account/AccountServerSettingsUpdater.kt @@ -1,10 +1,16 @@ package com.fsck.k9.account +import app.k9mail.core.common.mail.Protocols import app.k9mail.feature.account.edit.AccountEditExternalContract import app.k9mail.feature.account.edit.AccountEditExternalContract.AccountUpdaterFailure import app.k9mail.feature.account.edit.AccountEditExternalContract.AccountUpdaterResult import com.fsck.k9.logging.Timber import com.fsck.k9.mail.ServerSettings +import com.fsck.k9.mail.store.imap.ImapStoreSettings +import com.fsck.k9.mail.store.imap.ImapStoreSettings.autoDetectNamespace +import com.fsck.k9.mail.store.imap.ImapStoreSettings.isSendClientId +import com.fsck.k9.mail.store.imap.ImapStoreSettings.isUseCompression +import com.fsck.k9.mail.store.imap.ImapStoreSettings.pathPrefix import com.fsck.k9.preferences.AccountManager import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers @@ -42,7 +48,18 @@ class AccountServerSettingsUpdater( ) if (isIncoming) { - account.incomingServerSettings = serverSettings + if (serverSettings.type == Protocols.IMAP) { + account.useCompression = serverSettings.isUseCompression + account.isSendClientIdEnabled = serverSettings.isSendClientId + account.incomingServerSettings = serverSettings.copy( + extra = ImapStoreSettings.createExtra( + autoDetectNamespace = serverSettings.autoDetectNamespace, + pathPrefix = serverSettings.pathPrefix, + ), + ) + } else { + account.incomingServerSettings = serverSettings + } } else { account.outgoingServerSettings = serverSettings } diff --git a/app/k9mail/src/main/java/com/fsck/k9/account/AccountStateLoader.kt b/app/k9mail/src/main/java/com/fsck/k9/account/AccountStateLoader.kt index 71b30ec470e..8fb54061fe0 100644 --- a/app/k9mail/src/main/java/com/fsck/k9/account/AccountStateLoader.kt +++ b/app/k9mail/src/main/java/com/fsck/k9/account/AccountStateLoader.kt @@ -1,9 +1,13 @@ package com.fsck.k9.account +import app.k9mail.core.common.mail.Protocols import app.k9mail.feature.account.common.AccountCommonExternalContract import app.k9mail.feature.account.common.domain.entity.AccountState import app.k9mail.feature.account.common.domain.entity.AuthorizationState +import com.fsck.k9.Account +import com.fsck.k9.backends.toImapServerSettings import com.fsck.k9.logging.Timber +import com.fsck.k9.mail.ServerSettings import com.fsck.k9.preferences.AccountManager import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers @@ -36,9 +40,15 @@ class AccountStateLoader( return AccountState( uuid = account.uuid, emailAddress = account.email, - incomingServerSettings = account.incomingServerSettings, + incomingServerSettings = account.incomingServerSettingsExtra, outgoingServerSettings = account.outgoingServerSettings, authorizationState = AuthorizationState(account.oAuthState), ) } } + +private val Account.incomingServerSettingsExtra: ServerSettings + get() = when (incomingServerSettings.type) { + Protocols.IMAP -> toImapServerSettings() + else -> incomingServerSettings + } diff --git a/app/k9mail/src/test/java/com/fsck/k9/account/AccountStateLoaderTest.kt b/app/k9mail/src/test/java/com/fsck/k9/account/AccountStateLoaderTest.kt index 234b62a7016..e055a89aca3 100644 --- a/app/k9mail/src/test/java/com/fsck/k9/account/AccountStateLoaderTest.kt +++ b/app/k9mail/src/test/java/com/fsck/k9/account/AccountStateLoaderTest.kt @@ -54,9 +54,9 @@ class AccountStateLoaderTest { private companion object { val INCOMING_SERVER_SETTINGS = ServerSettings( - type = "imap", - host = "imap.example.org", - port = 143, + type = "pop3", + host = "pop.example.org", + port = 465, connectionSecurity = ConnectionSecurity.SSL_TLS_REQUIRED, authenticationType = AuthType.PLAIN, username = "username", diff --git a/app/k9mail/src/test/java/com/fsck/k9/account/AccountUpdaterTest.kt b/app/k9mail/src/test/java/com/fsck/k9/account/AccountUpdaterTest.kt index 8d9ef1ac2ff..3045b3c2d0d 100644 --- a/app/k9mail/src/test/java/com/fsck/k9/account/AccountUpdaterTest.kt +++ b/app/k9mail/src/test/java/com/fsck/k9/account/AccountUpdaterTest.kt @@ -96,9 +96,9 @@ class AccountUpdaterTest { const val ACCOUNT_UUID = "uuid" val INCOMING_SERVER_SETTINGS = ServerSettings( - type = "imap", - host = "imap.example.org", - port = 143, + type = "pop3", + host = "pop.example.org", + port = 465, connectionSecurity = ConnectionSecurity.SSL_TLS_REQUIRED, authenticationType = AuthType.PLAIN, username = "username", diff --git a/core/ui/compose/designsystem/src/main/kotlin/app/k9mail/core/ui/compose/designsystem/molecule/ContentLoadingErrorView.kt b/core/ui/compose/designsystem/src/main/kotlin/app/k9mail/core/ui/compose/designsystem/molecule/ContentLoadingErrorView.kt index 3985665b85b..4378fc544e2 100644 --- a/core/ui/compose/designsystem/src/main/kotlin/app/k9mail/core/ui/compose/designsystem/molecule/ContentLoadingErrorView.kt +++ b/core/ui/compose/designsystem/src/main/kotlin/app/k9mail/core/ui/compose/designsystem/molecule/ContentLoadingErrorView.kt @@ -1,7 +1,6 @@ package app.k9mail.core.ui.compose.designsystem.molecule import androidx.compose.animation.AnimatedContent -import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize @@ -14,7 +13,6 @@ import androidx.compose.ui.tooling.preview.Preview import app.k9mail.core.ui.compose.designsystem.atom.text.TextSubtitle1 import app.k9mail.core.ui.compose.theme.PreviewWithThemes -@OptIn(ExperimentalAnimationApi::class) @Composable fun ContentLoadingErrorView( state: ContentLoadingErrorState, diff --git a/core/ui/compose/designsystem/src/main/kotlin/app/k9mail/core/ui/compose/designsystem/molecule/ErrorView.kt b/core/ui/compose/designsystem/src/main/kotlin/app/k9mail/core/ui/compose/designsystem/molecule/ErrorView.kt index d60281fcef9..6bc8f2f0908 100644 --- a/core/ui/compose/designsystem/src/main/kotlin/app/k9mail/core/ui/compose/designsystem/molecule/ErrorView.kt +++ b/core/ui/compose/designsystem/src/main/kotlin/app/k9mail/core/ui/compose/designsystem/molecule/ErrorView.kt @@ -4,7 +4,9 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.material.Icon import androidx.compose.runtime.Composable @@ -26,7 +28,7 @@ fun ErrorView( title: String, modifier: Modifier = Modifier, message: String? = null, - onRetry: () -> Unit = { }, + onRetry: (() -> Unit)? = null, contentAlignment: Alignment = Alignment.Center, ) { Box( @@ -38,44 +40,45 @@ fun ErrorView( Column( modifier = Modifier .fillMaxWidth() - .padding( - vertical = MainTheme.spacings.default, - horizontal = MainTheme.spacings.double, - ), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(MainTheme.spacings.default), + .padding(MainTheme.spacings.double), ) { - Icon( - imageVector = Icons.Filled.error, - contentDescription = null, - tint = MainTheme.colors.error, - modifier = Modifier.padding(top = MainTheme.spacings.default), - ) - TextSubtitle1( - text = title, - modifier = Modifier.padding(bottom = MainTheme.spacings.default), - ) + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(MainTheme.spacings.default), + ) { + Icon( + imageVector = Icons.Filled.error, + contentDescription = null, + tint = MainTheme.colors.error, + ) + TextSubtitle1( + text = title, + ) + } + if (message != null) { + Spacer(modifier = Modifier.height(MainTheme.spacings.double)) TextBody2( text = message, - modifier = Modifier - .fillMaxWidth(), + modifier = Modifier.fillMaxWidth(), ) } - Row( - modifier = Modifier - .fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.End, - ) { - ButtonText( - text = stringResource(id = R.string.designsystem_molecule_error_view_button_retry), - onClick = onRetry, - contentPadding = buttonContentPadding( - start = MainTheme.spacings.double, - end = MainTheme.spacings.double, - ), - ) + if (onRetry != null) { + Spacer(modifier = Modifier.height(MainTheme.spacings.default)) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.End, + ) { + ButtonText( + text = stringResource(id = R.string.designsystem_molecule_error_view_button_retry), + onClick = onRetry, + contentPadding = buttonContentPadding( + start = MainTheme.spacings.double, + end = MainTheme.spacings.double, + ), + ) + } } } } @@ -101,3 +104,26 @@ internal fun ErrorViewWithMessagePreview() { ) } } + +@Preview(showBackground = true) +@Composable +internal fun ErrorViewWithRetryPreview() { + PreviewWithThemes { + ErrorView( + title = "Error", + onRetry = {}, + ) + } +} + +@Preview(showBackground = true) +@Composable +internal fun ErrorViewWithRetryAndMessagePreview() { + PreviewWithThemes { + ErrorView( + title = "Error", + message = "Something went wrong.", + onRetry = {}, + ) + } +} diff --git a/core/ui/compose/designsystem/src/main/kotlin/app/k9mail/core/ui/compose/designsystem/molecule/LoadingView.kt b/core/ui/compose/designsystem/src/main/kotlin/app/k9mail/core/ui/compose/designsystem/molecule/LoadingView.kt index 3266398109a..57747732d9f 100644 --- a/core/ui/compose/designsystem/src/main/kotlin/app/k9mail/core/ui/compose/designsystem/molecule/LoadingView.kt +++ b/core/ui/compose/designsystem/src/main/kotlin/app/k9mail/core/ui/compose/designsystem/molecule/LoadingView.kt @@ -31,8 +31,7 @@ fun LoadingView( Column( modifier = Modifier .fillMaxWidth() - .padding(MainTheme.spacings.default) - .then(modifier), + .padding(MainTheme.spacings.double), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center, ) { diff --git a/core/ui/compose/testing/src/main/kotlin/app/k9mail/core/ui/compose/testing/mvi/MviTurbineExtension.kt b/core/ui/compose/testing/src/main/kotlin/app/k9mail/core/ui/compose/testing/mvi/MviTurbineExtension.kt index 23e77d1bccb..28b56e3720e 100644 --- a/core/ui/compose/testing/src/main/kotlin/app/k9mail/core/ui/compose/testing/mvi/MviTurbineExtension.kt +++ b/core/ui/compose/testing/src/main/kotlin/app/k9mail/core/ui/compose/testing/mvi/MviTurbineExtension.kt @@ -72,6 +72,36 @@ fun assertThatAndMviTurbinesConsumed( turbines.effectTurbine.ensureAllEventsConsumed() } +/** + * The `assertThatAndStateTurbineConsumed` function ensures that the assertion passed and + * all events in the state turbine have been consumed. + */ +suspend fun MviTurbines.assertThatAndStateTurbineConsumed( + assertion: Assert.() -> Unit, +) { + assertThat(stateTurbine.awaitItem()).all { + assertion() + } + + stateTurbine.ensureAllEventsConsumed() + effectTurbine.ensureAllEventsConsumed() +} + +/** + * The `assertThatAndEffectTurbineConsumed` function ensures that the assertion passed and + * all events in the effect turbine have been consumed. + */ +suspend fun MviTurbines.assertThatAndEffectTurbineConsumed( + assertion: Assert.() -> Unit, +) { + assertThat(effectTurbine.awaitItem()).all { + assertion() + } + + stateTurbine.ensureAllEventsConsumed() + effectTurbine.ensureAllEventsConsumed() +} + /** * A container class for the state and effect turbines of an MVI ViewModel. */ diff --git a/feature/account/common/src/main/kotlin/app/k9mail/feature/account/common/ui/loadingerror/LoadingErrorState.kt b/feature/account/common/src/main/kotlin/app/k9mail/feature/account/common/ui/loadingerror/LoadingErrorState.kt new file mode 100644 index 00000000000..67f41cda990 --- /dev/null +++ b/feature/account/common/src/main/kotlin/app/k9mail/feature/account/common/ui/loadingerror/LoadingErrorState.kt @@ -0,0 +1,21 @@ +package app.k9mail.feature.account.common.ui.loadingerror + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import app.k9mail.core.ui.compose.designsystem.molecule.ContentLoadingErrorState + +interface LoadingErrorState { + val isLoading: Boolean + val error: ERROR? +} + +@Composable +fun rememberContentLoadingErrorViewState( + state: LoadingErrorState, +) = remember(key1 = state.isLoading, key2 = state.error) { + when { + state.isLoading -> ContentLoadingErrorState.Loading + state.error != null -> ContentLoadingErrorState.Error + else -> ContentLoadingErrorState.Content + } +} diff --git a/feature/account/common/src/main/kotlin/app/k9mail/feature/account/common/ui/view/SuccessView.kt b/feature/account/common/src/main/kotlin/app/k9mail/feature/account/common/ui/view/SuccessView.kt index 4f822744123..a9bcee8b6a4 100644 --- a/feature/account/common/src/main/kotlin/app/k9mail/feature/account/common/ui/view/SuccessView.kt +++ b/feature/account/common/src/main/kotlin/app/k9mail/feature/account/common/ui/view/SuccessView.kt @@ -17,7 +17,7 @@ import app.k9mail.core.ui.compose.theme.MainTheme import app.k9mail.core.ui.compose.theme.PreviewWithThemes @Composable -internal fun SuccessView( +fun SuccessView( message: String, modifier: Modifier = Modifier, ) { diff --git a/feature/account/edit/build.gradle.kts b/feature/account/edit/build.gradle.kts index 5ef5291d048..0b73305c754 100644 --- a/feature/account/edit/build.gradle.kts +++ b/feature/account/edit/build.gradle.kts @@ -29,4 +29,5 @@ dependencies { implementation(projects.feature.account.server.validation) testImplementation(projects.core.ui.compose.testing) + testImplementation(projects.mail.protocols.imap) } diff --git a/feature/account/edit/src/main/kotlin/app/k9mail/feature/account/edit/AccountEditExternalContract.kt b/feature/account/edit/src/main/kotlin/app/k9mail/feature/account/edit/AccountEditExternalContract.kt index 6ce49b14398..3f79ff17ac8 100644 --- a/feature/account/edit/src/main/kotlin/app/k9mail/feature/account/edit/AccountEditExternalContract.kt +++ b/feature/account/edit/src/main/kotlin/app/k9mail/feature/account/edit/AccountEditExternalContract.kt @@ -5,7 +5,7 @@ import com.fsck.k9.mail.ServerSettings interface AccountEditExternalContract { sealed interface AccountUpdaterResult { - data class Success(val message: String) : AccountUpdaterResult + data class Success(val accountUuid: String) : AccountUpdaterResult data class Failure(val error: AccountUpdaterFailure) : AccountUpdaterResult } diff --git a/feature/account/edit/src/main/kotlin/app/k9mail/feature/account/edit/AccountEditModule.kt b/feature/account/edit/src/main/kotlin/app/k9mail/feature/account/edit/AccountEditModule.kt index ca91aa4b0f0..7c641703996 100644 --- a/feature/account/edit/src/main/kotlin/app/k9mail/feature/account/edit/AccountEditModule.kt +++ b/feature/account/edit/src/main/kotlin/app/k9mail/feature/account/edit/AccountEditModule.kt @@ -2,9 +2,13 @@ package app.k9mail.feature.account.edit import app.k9mail.feature.account.common.featureAccountCommonModule import app.k9mail.feature.account.edit.domain.AccountEditDomainContract +import app.k9mail.feature.account.edit.domain.usecase.GetAccountState import app.k9mail.feature.account.edit.domain.usecase.LoadAccountState -import app.k9mail.feature.account.edit.ui.EditIncomingServerSettingsViewModel -import app.k9mail.feature.account.edit.ui.EditOutgoingServerSettingsViewModel +import app.k9mail.feature.account.edit.domain.usecase.SaveServerSettings +import app.k9mail.feature.account.edit.ui.server.settings.modify.ModifyIncomingServerSettingsViewModel +import app.k9mail.feature.account.edit.ui.server.settings.modify.ModifyOutgoingServerSettingsViewModel +import app.k9mail.feature.account.edit.ui.server.settings.save.SaveIncomingServerSettingsViewModel +import app.k9mail.feature.account.edit.ui.server.settings.save.SaveOutgoingServerSettingsViewModel import app.k9mail.feature.account.oauth.featureAccountOAuthModule import app.k9mail.feature.account.server.certificate.featureAccountServerCertificateModule import app.k9mail.feature.account.server.settings.featureAccountServerSettingsModule @@ -28,8 +32,21 @@ val featureAccountEditModule = module { ) } + factory { + GetAccountState( + accountStateRepository = get(), + ) + } + + factory { + SaveServerSettings( + getAccountState = get(), + serverSettingsUpdater = get(), + ) + } + viewModel { (accountUuid: String) -> - EditIncomingServerSettingsViewModel( + ModifyIncomingServerSettingsViewModel( accountUuid = accountUuid, accountStateLoader = get(), validator = get(), @@ -38,11 +55,25 @@ val featureAccountEditModule = module { } viewModel { (accountUuid: String) -> - EditOutgoingServerSettingsViewModel( + ModifyOutgoingServerSettingsViewModel( accountUuid = accountUuid, accountStateLoader = get(), validator = get(), accountStateRepository = get(), ) } + + viewModel { (accountUuid: String) -> + SaveIncomingServerSettingsViewModel( + accountUuid = accountUuid, + saveServerSettings = get(), + ) + } + + viewModel { (accountUuid: String) -> + SaveOutgoingServerSettingsViewModel( + accountUuid = accountUuid, + saveServerSettings = get(), + ) + } } diff --git a/feature/account/edit/src/main/kotlin/app/k9mail/feature/account/edit/domain/AccountEditDomainContract.kt b/feature/account/edit/src/main/kotlin/app/k9mail/feature/account/edit/domain/AccountEditDomainContract.kt index 58dd6930132..19889e9028e 100644 --- a/feature/account/edit/src/main/kotlin/app/k9mail/feature/account/edit/domain/AccountEditDomainContract.kt +++ b/feature/account/edit/src/main/kotlin/app/k9mail/feature/account/edit/domain/AccountEditDomainContract.kt @@ -9,5 +9,13 @@ interface AccountEditDomainContract { fun interface LoadAccountState { suspend fun execute(accountUuid: String): AccountState } + + fun interface GetAccountState { + suspend fun execute(accountUuid: String): AccountState + } + + fun interface SaveServerSettings { + suspend fun execute(accountUuid: String, isIncoming: Boolean) + } } } diff --git a/feature/account/edit/src/main/kotlin/app/k9mail/feature/account/edit/domain/usecase/GetAccountState.kt b/feature/account/edit/src/main/kotlin/app/k9mail/feature/account/edit/domain/usecase/GetAccountState.kt new file mode 100644 index 00000000000..97e0e1d0188 --- /dev/null +++ b/feature/account/edit/src/main/kotlin/app/k9mail/feature/account/edit/domain/usecase/GetAccountState.kt @@ -0,0 +1,18 @@ +package app.k9mail.feature.account.edit.domain.usecase + +import app.k9mail.feature.account.common.domain.AccountDomainContract +import app.k9mail.feature.account.common.domain.entity.AccountState +import app.k9mail.feature.account.edit.domain.AccountEditDomainContract.UseCase + +class GetAccountState( + private val accountStateRepository: AccountDomainContract.AccountStateRepository, +) : UseCase.GetAccountState { + override suspend fun execute(accountUuid: String): AccountState { + val accountState = accountStateRepository.getState() + return if (accountState.uuid == accountUuid) { + accountState + } else { + error("Account state for $accountUuid not found") + } + } +} diff --git a/feature/account/edit/src/main/kotlin/app/k9mail/feature/account/edit/domain/usecase/LoadAccountState.kt b/feature/account/edit/src/main/kotlin/app/k9mail/feature/account/edit/domain/usecase/LoadAccountState.kt index 832fe6d9347..7e02532a904 100644 --- a/feature/account/edit/src/main/kotlin/app/k9mail/feature/account/edit/domain/usecase/LoadAccountState.kt +++ b/feature/account/edit/src/main/kotlin/app/k9mail/feature/account/edit/domain/usecase/LoadAccountState.kt @@ -10,20 +10,14 @@ class LoadAccountState( private val accountStateRepository: AccountDomainContract.AccountStateRepository, ) : AccountEditDomainContract.UseCase.LoadAccountState { override suspend fun execute(accountUuid: String): AccountState { - val accountState = accountStateRepository.getState() - return if (accountState.uuid == accountUuid) { - accountState + val accountState = accountStateLoader.loadAccountState(accountUuid) + + if (accountState != null) { + accountStateRepository.setState(accountState) } else { - loadState(accountUuid) + error("Account state for $accountUuid not found") } - } - private suspend fun loadState(accountUuid: String): AccountState { - val accountState = accountStateLoader.loadAccountState(accountUuid) - return if (accountState != null) { - accountState - } else { - AccountState(uuid = accountUuid) - }.also { accountStateRepository.setState(it) } + return accountState } } diff --git a/feature/account/edit/src/main/kotlin/app/k9mail/feature/account/edit/domain/usecase/SaveServerSettings.kt b/feature/account/edit/src/main/kotlin/app/k9mail/feature/account/edit/domain/usecase/SaveServerSettings.kt new file mode 100644 index 00000000000..252f095283c --- /dev/null +++ b/feature/account/edit/src/main/kotlin/app/k9mail/feature/account/edit/domain/usecase/SaveServerSettings.kt @@ -0,0 +1,42 @@ +package app.k9mail.feature.account.edit.domain.usecase + +import app.k9mail.feature.account.edit.AccountEditExternalContract.AccountServerSettingsUpdater +import app.k9mail.feature.account.edit.AccountEditExternalContract.AccountUpdaterResult +import app.k9mail.feature.account.edit.domain.AccountEditDomainContract.UseCase +import com.fsck.k9.mail.ServerSettings + +class SaveServerSettings( + private val getAccountState: UseCase.GetAccountState, + private val serverSettingsUpdater: AccountServerSettingsUpdater, +) : UseCase.SaveServerSettings { + override suspend fun execute(accountUuid: String, isIncoming: Boolean) { + val serverSettings = loadServerSettings(accountUuid, isIncoming) + + if (serverSettings != null) { + updateServerSettings(accountUuid, isIncoming, serverSettings) + } else { + error("Server settings not found") + } + } + + private suspend fun loadServerSettings(accountUuid: String, isIncoming: Boolean): ServerSettings? { + val accountState = getAccountState.execute(accountUuid) + return if (isIncoming) { + accountState.incomingServerSettings + } else { + accountState.outgoingServerSettings + } + } + + private suspend fun updateServerSettings(accountUuid: String, isIncoming: Boolean, serverSettings: ServerSettings) { + val result = serverSettingsUpdater.updateServerSettings( + accountUuid = accountUuid, + isIncoming = isIncoming, + serverSettings = serverSettings, + ) + + if (result is AccountUpdaterResult.Failure) { + error("Server settings update failed") + } + } +} diff --git a/feature/account/edit/src/main/kotlin/app/k9mail/feature/account/edit/navigation/AccountEditNavigation.kt b/feature/account/edit/src/main/kotlin/app/k9mail/feature/account/edit/navigation/AccountEditNavigation.kt index 2692bd103f7..2f07177464d 100644 --- a/feature/account/edit/src/main/kotlin/app/k9mail/feature/account/edit/navigation/AccountEditNavigation.kt +++ b/feature/account/edit/src/main/kotlin/app/k9mail/feature/account/edit/navigation/AccountEditNavigation.kt @@ -6,23 +6,23 @@ import androidx.navigation.NavType import androidx.navigation.navArgument import app.k9mail.core.ui.compose.common.navigation.deepLinkComposable import app.k9mail.core.ui.compose.common.navigation.getStringArgument -import app.k9mail.feature.account.edit.ui.EditIncomingServerSettingsNavHost -import app.k9mail.feature.account.edit.ui.EditOutgoingServerSettingsNavHost +import app.k9mail.feature.account.edit.ui.server.settings.EditIncomingServerSettingsNavHost +import app.k9mail.feature.account.edit.ui.server.settings.EditOutgoingServerSettingsNavHost internal const val ARGUMENT_ACCOUNT_UUID = "accountUuid" -const val NAVIGATION_ROUTE_ACCOUNT_EDIT_CONFIG_INCOMING = "account/edit/config/incoming/{accountUuid}" -const val NAVIGATION_ROUTE_ACCOUNT_EDIT_CONFIG_OUTGOING = "account/edit/config/outgoing/{accountUuid}" +const val NAVIGATION_ROUTE_ACCOUNT_EDIT_SERVER_SETTINGS_INCOMING = "account/edit/server/settings/incoming/{accountUuid}" +const val NAVIGATION_ROUTE_ACCOUNT_EDIT_SERVER_SETTINGS_OUTGOING = "account/edit/server/settings/outgoing/{accountUuid}" -fun NavController.navigateToAccountEditConfigIncoming(accountUuid: String) { +fun NavController.navigateToAccountEditIncomingServerSettings(accountUuid: String) { navigate( - route = NAVIGATION_ROUTE_ACCOUNT_EDIT_CONFIG_INCOMING.withAccountUuid(accountUuid), + route = NAVIGATION_ROUTE_ACCOUNT_EDIT_SERVER_SETTINGS_INCOMING.withAccountUuid(accountUuid), ) } -fun NavController.navigateToAccountEditConfigOutgoing(accountUuid: String) { +fun NavController.navigateToAccountEditOutgoingServerSettings(accountUuid: String) { navigate( - route = NAVIGATION_ROUTE_ACCOUNT_EDIT_CONFIG_OUTGOING.withAccountUuid(accountUuid), + route = NAVIGATION_ROUTE_ACCOUNT_EDIT_SERVER_SETTINGS_OUTGOING.withAccountUuid(accountUuid), ) } @@ -31,7 +31,7 @@ fun NavGraphBuilder.accountEditRoute( onFinish: () -> Unit, ) { deepLinkComposable( - route = NAVIGATION_ROUTE_ACCOUNT_EDIT_CONFIG_INCOMING, + route = NAVIGATION_ROUTE_ACCOUNT_EDIT_SERVER_SETTINGS_INCOMING, arguments = listOf( navArgument(ARGUMENT_ACCOUNT_UUID) { type = NavType.StringType @@ -46,7 +46,7 @@ fun NavGraphBuilder.accountEditRoute( ) } deepLinkComposable( - route = NAVIGATION_ROUTE_ACCOUNT_EDIT_CONFIG_OUTGOING, + route = NAVIGATION_ROUTE_ACCOUNT_EDIT_SERVER_SETTINGS_OUTGOING, arguments = listOf( navArgument(ARGUMENT_ACCOUNT_UUID) { type = NavType.StringType diff --git a/feature/account/edit/src/main/kotlin/app/k9mail/feature/account/edit/ui/EditIncomingServerSettingsNavHost.kt b/feature/account/edit/src/main/kotlin/app/k9mail/feature/account/edit/ui/EditIncomingServerSettingsNavHost.kt deleted file mode 100644 index 329fdceee96..00000000000 --- a/feature/account/edit/src/main/kotlin/app/k9mail/feature/account/edit/ui/EditIncomingServerSettingsNavHost.kt +++ /dev/null @@ -1,52 +0,0 @@ -package app.k9mail.feature.account.edit.ui - -import androidx.compose.runtime.Composable -import androidx.navigation.NavController -import androidx.navigation.compose.NavHost -import androidx.navigation.compose.composable -import androidx.navigation.compose.rememberNavController -import app.k9mail.feature.account.server.settings.ui.incoming.IncomingServerSettingsScreen -import app.k9mail.feature.account.server.validation.ui.IncomingServerValidationViewModel -import app.k9mail.feature.account.server.validation.ui.ServerValidationScreen -import org.koin.androidx.compose.koinViewModel -import org.koin.core.parameter.parametersOf - -private const val NESTED_NAVIGATION_ROUTE_CONFIG = "config" -private const val NESTED_NAVIGATION_ROUTE_VALIDATION = "validation" - -private fun NavController.navigateToValidation() { - navigate(NESTED_NAVIGATION_ROUTE_VALIDATION) -} - -@Composable -fun EditIncomingServerSettingsNavHost( - accountUuid: String, - onFinish: () -> Unit, - onBack: () -> Unit, -) { - val navController = rememberNavController() - - NavHost( - navController = navController, - startDestination = NESTED_NAVIGATION_ROUTE_CONFIG, - ) { - composable(route = NESTED_NAVIGATION_ROUTE_CONFIG) { - IncomingServerSettingsScreen( - onBack = onBack, - onNext = { navController.navigateToValidation() }, - viewModel = koinViewModel { - parametersOf(accountUuid) - }, - ) - } - composable(route = NESTED_NAVIGATION_ROUTE_VALIDATION) { - ServerValidationScreen( - onBack = { navController.popBackStack() }, - onNext = onFinish, - viewModel = koinViewModel { - parametersOf(accountUuid) - }, - ) - } - } -} diff --git a/feature/account/edit/src/main/kotlin/app/k9mail/feature/account/edit/ui/server/settings/EditIncomingServerSettingsNavHost.kt b/feature/account/edit/src/main/kotlin/app/k9mail/feature/account/edit/ui/server/settings/EditIncomingServerSettingsNavHost.kt new file mode 100644 index 00000000000..13fe8b10e5a --- /dev/null +++ b/feature/account/edit/src/main/kotlin/app/k9mail/feature/account/edit/ui/server/settings/EditIncomingServerSettingsNavHost.kt @@ -0,0 +1,69 @@ +package app.k9mail.feature.account.edit.ui.server.settings + +import androidx.compose.runtime.Composable +import androidx.navigation.NavController +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import app.k9mail.feature.account.edit.ui.server.settings.modify.ModifyIncomingServerSettingsViewModel +import app.k9mail.feature.account.edit.ui.server.settings.save.SaveIncomingServerSettingsViewModel +import app.k9mail.feature.account.edit.ui.server.settings.save.SaveServerSettingsScreen +import app.k9mail.feature.account.server.settings.ui.incoming.IncomingServerSettingsScreen +import app.k9mail.feature.account.server.validation.ui.IncomingServerValidationViewModel +import app.k9mail.feature.account.server.validation.ui.ServerValidationScreen +import org.koin.androidx.compose.koinViewModel +import org.koin.core.parameter.parametersOf + +private const val NESTED_NAVIGATION_ROUTE_MODIFY = "modify" +private const val NESTED_NAVIGATION_ROUTE_VALIDATE = "validate" +private const val NESTED_NAVIGATION_ROUTE_SAVE = "save" + +private fun NavController.navigateToValidate() { + navigate(NESTED_NAVIGATION_ROUTE_VALIDATE) +} + +private fun NavController.navigateToSave() { + navigate(NESTED_NAVIGATION_ROUTE_SAVE) +} + +@Composable +fun EditIncomingServerSettingsNavHost( + accountUuid: String, + onFinish: () -> Unit, + onBack: () -> Unit, +) { + val navController = rememberNavController() + + NavHost( + navController = navController, + startDestination = NESTED_NAVIGATION_ROUTE_MODIFY, + ) { + composable(route = NESTED_NAVIGATION_ROUTE_MODIFY) { + IncomingServerSettingsScreen( + onBack = onBack, + onNext = { navController.navigateToValidate() }, + viewModel = koinViewModel { + parametersOf(accountUuid) + }, + ) + } + composable(route = NESTED_NAVIGATION_ROUTE_VALIDATE) { + ServerValidationScreen( + onBack = { navController.popBackStack() }, + onNext = { navController.navigateToSave() }, + viewModel = koinViewModel { + parametersOf(accountUuid) + }, + ) + } + composable(route = NESTED_NAVIGATION_ROUTE_SAVE) { + SaveServerSettingsScreen( + onNext = onFinish, + onBack = { navController.popBackStack(route = NESTED_NAVIGATION_ROUTE_MODIFY, inclusive = false) }, + viewModel = koinViewModel { + parametersOf(accountUuid) + }, + ) + } + } +} diff --git a/feature/account/edit/src/main/kotlin/app/k9mail/feature/account/edit/ui/EditOutgoingServerSettingsNavHost.kt b/feature/account/edit/src/main/kotlin/app/k9mail/feature/account/edit/ui/server/settings/EditOutgoingServerSettingsNavHost.kt similarity index 52% rename from feature/account/edit/src/main/kotlin/app/k9mail/feature/account/edit/ui/EditOutgoingServerSettingsNavHost.kt rename to feature/account/edit/src/main/kotlin/app/k9mail/feature/account/edit/ui/server/settings/EditOutgoingServerSettingsNavHost.kt index 5b6d5ed5e7d..377cb0a52a9 100644 --- a/feature/account/edit/src/main/kotlin/app/k9mail/feature/account/edit/ui/EditOutgoingServerSettingsNavHost.kt +++ b/feature/account/edit/src/main/kotlin/app/k9mail/feature/account/edit/ui/server/settings/EditOutgoingServerSettingsNavHost.kt @@ -1,10 +1,12 @@ -package app.k9mail.feature.account.edit.ui +package app.k9mail.feature.account.edit.ui.server.settings import androidx.compose.runtime.Composable import androidx.navigation.NavController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController +import app.k9mail.feature.account.edit.ui.server.settings.save.SaveOutgoingServerSettingsViewModel +import app.k9mail.feature.account.edit.ui.server.settings.save.SaveServerSettingsScreen import app.k9mail.feature.account.server.settings.ui.outgoing.OutgoingServerSettingsScreen import app.k9mail.feature.account.server.settings.ui.outgoing.OutgoingServerSettingsViewModel import app.k9mail.feature.account.server.validation.ui.OutgoingServerValidationViewModel @@ -12,11 +14,16 @@ import app.k9mail.feature.account.server.validation.ui.ServerValidationScreen import org.koin.androidx.compose.koinViewModel import org.koin.core.parameter.parametersOf -private const val NESTED_NAVIGATION_ROUTE_CONFIG = "config" -private const val NESTED_NAVIGATION_ROUTE_VALIDATION = "validation" +private const val NESTED_NAVIGATION_ROUTE_MODIFY = "modify" +private const val NESTED_NAVIGATION_ROUTE_VALIDATE = "validate" +private const val NESTED_NAVIGATION_ROUTE_SAVE = "save" -private fun NavController.navigateToValidation() { - navigate(NESTED_NAVIGATION_ROUTE_VALIDATION) +private fun NavController.navigateToValidate() { + navigate(NESTED_NAVIGATION_ROUTE_VALIDATE) +} + +private fun NavController.navigateToSave() { + navigate(NESTED_NAVIGATION_ROUTE_SAVE) } @Composable @@ -29,25 +36,34 @@ fun EditOutgoingServerSettingsNavHost( NavHost( navController = navController, - startDestination = NESTED_NAVIGATION_ROUTE_CONFIG, + startDestination = NESTED_NAVIGATION_ROUTE_MODIFY, ) { - composable(route = NESTED_NAVIGATION_ROUTE_CONFIG) { + composable(route = NESTED_NAVIGATION_ROUTE_MODIFY) { OutgoingServerSettingsScreen( onBack = onBack, - onNext = { navController.navigateToValidation() }, + onNext = { navController.navigateToValidate() }, viewModel = koinViewModel { parametersOf(accountUuid) }, ) } - composable(route = NESTED_NAVIGATION_ROUTE_VALIDATION) { + composable(route = NESTED_NAVIGATION_ROUTE_VALIDATE) { ServerValidationScreen( onBack = { navController.popBackStack() }, - onNext = onFinish, + onNext = { navController.navigateToSave() }, viewModel = koinViewModel { parametersOf(accountUuid) }, ) } + composable(route = NESTED_NAVIGATION_ROUTE_SAVE) { + SaveServerSettingsScreen( + onNext = onFinish, + onBack = { navController.popBackStack(route = NESTED_NAVIGATION_ROUTE_MODIFY, inclusive = false) }, + viewModel = koinViewModel { + parametersOf(accountUuid) + }, + ) + } } } diff --git a/feature/account/edit/src/main/kotlin/app/k9mail/feature/account/edit/ui/EditIncomingServerSettingsViewModel.kt b/feature/account/edit/src/main/kotlin/app/k9mail/feature/account/edit/ui/server/settings/modify/ModifyIncomingServerSettingsViewModel.kt similarity index 86% rename from feature/account/edit/src/main/kotlin/app/k9mail/feature/account/edit/ui/EditIncomingServerSettingsViewModel.kt rename to feature/account/edit/src/main/kotlin/app/k9mail/feature/account/edit/ui/server/settings/modify/ModifyIncomingServerSettingsViewModel.kt index e873747e379..76cdb82dc23 100644 --- a/feature/account/edit/src/main/kotlin/app/k9mail/feature/account/edit/ui/EditIncomingServerSettingsViewModel.kt +++ b/feature/account/edit/src/main/kotlin/app/k9mail/feature/account/edit/ui/server/settings/modify/ModifyIncomingServerSettingsViewModel.kt @@ -1,4 +1,4 @@ -package app.k9mail.feature.account.edit.ui +package app.k9mail.feature.account.edit.ui.server.settings.modify import androidx.lifecycle.viewModelScope import app.k9mail.feature.account.common.domain.AccountDomainContract @@ -6,10 +6,10 @@ import app.k9mail.feature.account.common.domain.entity.InteractionMode import app.k9mail.feature.account.edit.domain.AccountEditDomainContract import app.k9mail.feature.account.server.settings.ui.incoming.IncomingServerSettingsContract import app.k9mail.feature.account.server.settings.ui.incoming.IncomingServerSettingsViewModel -import app.k9mail.feature.account.server.settings.ui.incoming.toIncomingConfigState +import app.k9mail.feature.account.server.settings.ui.incoming.toIncomingServerSettingsState import kotlinx.coroutines.launch -class EditIncomingServerSettingsViewModel( +class ModifyIncomingServerSettingsViewModel( val accountUuid: String, private val accountStateLoader: AccountEditDomainContract.UseCase.LoadAccountState, validator: IncomingServerSettingsContract.Validator, @@ -27,7 +27,7 @@ class EditIncomingServerSettingsViewModel( val state = accountStateLoader.execute(accountUuid) updateState { - state.toIncomingConfigState() + state.toIncomingServerSettingsState() } } } diff --git a/feature/account/edit/src/main/kotlin/app/k9mail/feature/account/edit/ui/EditOutgoingServerSettingsViewModel.kt b/feature/account/edit/src/main/kotlin/app/k9mail/feature/account/edit/ui/server/settings/modify/ModifyOutgoingServerSettingsViewModel.kt similarity index 86% rename from feature/account/edit/src/main/kotlin/app/k9mail/feature/account/edit/ui/EditOutgoingServerSettingsViewModel.kt rename to feature/account/edit/src/main/kotlin/app/k9mail/feature/account/edit/ui/server/settings/modify/ModifyOutgoingServerSettingsViewModel.kt index a6f8287f69e..0648a225877 100644 --- a/feature/account/edit/src/main/kotlin/app/k9mail/feature/account/edit/ui/EditOutgoingServerSettingsViewModel.kt +++ b/feature/account/edit/src/main/kotlin/app/k9mail/feature/account/edit/ui/server/settings/modify/ModifyOutgoingServerSettingsViewModel.kt @@ -1,4 +1,4 @@ -package app.k9mail.feature.account.edit.ui +package app.k9mail.feature.account.edit.ui.server.settings.modify import androidx.lifecycle.viewModelScope import app.k9mail.feature.account.common.domain.AccountDomainContract @@ -6,10 +6,10 @@ import app.k9mail.feature.account.common.domain.entity.InteractionMode import app.k9mail.feature.account.edit.domain.AccountEditDomainContract import app.k9mail.feature.account.server.settings.ui.outgoing.OutgoingServerSettingsContract import app.k9mail.feature.account.server.settings.ui.outgoing.OutgoingServerSettingsViewModel -import app.k9mail.feature.account.server.settings.ui.outgoing.toOutgoingConfigState +import app.k9mail.feature.account.server.settings.ui.outgoing.toOutgoingServerSettingsState import kotlinx.coroutines.launch -class EditOutgoingServerSettingsViewModel( +class ModifyOutgoingServerSettingsViewModel( val accountUuid: String, private val accountStateLoader: AccountEditDomainContract.UseCase.LoadAccountState, validator: OutgoingServerSettingsContract.Validator, @@ -26,7 +26,7 @@ class EditOutgoingServerSettingsViewModel( val state = accountStateLoader.execute(accountUuid) updateState { - state.toOutgoingConfigState() + state.toOutgoingServerSettingsState() } } } diff --git a/feature/account/edit/src/main/kotlin/app/k9mail/feature/account/edit/ui/server/settings/save/BaseSaveServerSettingsViewModel.kt b/feature/account/edit/src/main/kotlin/app/k9mail/feature/account/edit/ui/server/settings/save/BaseSaveServerSettingsViewModel.kt new file mode 100644 index 00000000000..8353ea5cd9b --- /dev/null +++ b/feature/account/edit/src/main/kotlin/app/k9mail/feature/account/edit/ui/server/settings/save/BaseSaveServerSettingsViewModel.kt @@ -0,0 +1,80 @@ +package app.k9mail.feature.account.edit.ui.server.settings.save + +import androidx.lifecycle.viewModelScope +import app.k9mail.core.ui.compose.common.mvi.BaseViewModel +import app.k9mail.feature.account.edit.domain.AccountEditDomainContract +import app.k9mail.feature.account.edit.ui.server.settings.save.SaveServerSettingsContract.Effect +import app.k9mail.feature.account.edit.ui.server.settings.save.SaveServerSettingsContract.Event +import app.k9mail.feature.account.edit.ui.server.settings.save.SaveServerSettingsContract.Failure +import app.k9mail.feature.account.edit.ui.server.settings.save.SaveServerSettingsContract.State +import app.k9mail.feature.account.edit.ui.server.settings.save.SaveServerSettingsContract.ViewModel +import kotlinx.coroutines.cancelChildren +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +private const val CONTINUE_NEXT_DELAY = 1500L + +abstract class BaseSaveServerSettingsViewModel( + val accountUuid: String, + override val isIncoming: Boolean, + private val saveServerSettings: AccountEditDomainContract.UseCase.SaveServerSettings, + initialState: State = State(), +) : BaseViewModel(initialState), + ViewModel { + + override fun event(event: Event) { + when (event) { + Event.SaveServerSettings -> onSaveServerSettings() + Event.OnNextClicked -> navigateNext() + Event.OnBackClicked -> navigateBack() + } + } + + @Suppress("TooGenericExceptionCaught") + private fun onSaveServerSettings() { + viewModelScope.launch { + try { + saveServerSettings.execute(accountUuid, isIncoming) + updateSuccess() + } catch (e: Exception) { + updateFailure(Failure.SaveServerSettingsFailed(e.message ?: "Unknown error")) + } + } + } + + private fun updateSuccess() { + updateState { + it.copy( + isLoading = false, + ) + } + + viewModelScope.launch { + delay(CONTINUE_NEXT_DELAY) + navigateNext() + } + } + + private fun updateFailure(failure: Failure) { + updateState { + it.copy( + error = failure, + isLoading = false, + ) + } + } + + private fun navigateNext() { + if (state.value.isLoading || state.value.error != null) return + + viewModelScope.coroutineContext.cancelChildren() + emitEffect(Effect.NavigateNext) + } + + private fun navigateBack() { + if (state.value.isLoading || state.value.error == null) return + + viewModelScope.coroutineContext.cancelChildren() + emitEffect(Effect.NavigateBack) + } +} diff --git a/feature/account/edit/src/main/kotlin/app/k9mail/feature/account/edit/ui/server/settings/save/SaveIncomingServerSettingsViewModel.kt b/feature/account/edit/src/main/kotlin/app/k9mail/feature/account/edit/ui/server/settings/save/SaveIncomingServerSettingsViewModel.kt new file mode 100644 index 00000000000..a5649efbabf --- /dev/null +++ b/feature/account/edit/src/main/kotlin/app/k9mail/feature/account/edit/ui/server/settings/save/SaveIncomingServerSettingsViewModel.kt @@ -0,0 +1,15 @@ +package app.k9mail.feature.account.edit.ui.server.settings.save + +import app.k9mail.feature.account.edit.domain.AccountEditDomainContract.UseCase +import app.k9mail.feature.account.edit.ui.server.settings.save.SaveServerSettingsContract.State + +class SaveIncomingServerSettingsViewModel( + accountUuid: String, + saveServerSettings: UseCase.SaveServerSettings, + initialState: State = State(), +) : BaseSaveServerSettingsViewModel( + accountUuid = accountUuid, + isIncoming = true, + saveServerSettings = saveServerSettings, + initialState = initialState, +) diff --git a/feature/account/edit/src/main/kotlin/app/k9mail/feature/account/edit/ui/server/settings/save/SaveOutgoingServerSettingsViewModel.kt b/feature/account/edit/src/main/kotlin/app/k9mail/feature/account/edit/ui/server/settings/save/SaveOutgoingServerSettingsViewModel.kt new file mode 100644 index 00000000000..ba38d2cc5e1 --- /dev/null +++ b/feature/account/edit/src/main/kotlin/app/k9mail/feature/account/edit/ui/server/settings/save/SaveOutgoingServerSettingsViewModel.kt @@ -0,0 +1,15 @@ +package app.k9mail.feature.account.edit.ui.server.settings.save + +import app.k9mail.feature.account.edit.domain.AccountEditDomainContract.UseCase +import app.k9mail.feature.account.edit.ui.server.settings.save.SaveServerSettingsContract.State + +class SaveOutgoingServerSettingsViewModel( + accountUuid: String, + saveServerSettings: UseCase.SaveServerSettings, + initialState: State = State(), +) : BaseSaveServerSettingsViewModel( + accountUuid = accountUuid, + isIncoming = false, + saveServerSettings = saveServerSettings, + initialState = initialState, +) diff --git a/feature/account/edit/src/main/kotlin/app/k9mail/feature/account/edit/ui/server/settings/save/SaveServerSettingsContent.kt b/feature/account/edit/src/main/kotlin/app/k9mail/feature/account/edit/ui/server/settings/save/SaveServerSettingsContent.kt new file mode 100644 index 00000000000..de9ccc1efbe --- /dev/null +++ b/feature/account/edit/src/main/kotlin/app/k9mail/feature/account/edit/ui/server/settings/save/SaveServerSettingsContent.kt @@ -0,0 +1,97 @@ +package app.k9mail.feature.account.edit.ui.server.settings.save + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import app.k9mail.core.ui.compose.designsystem.molecule.ContentLoadingErrorView +import app.k9mail.core.ui.compose.designsystem.molecule.ErrorView +import app.k9mail.core.ui.compose.designsystem.molecule.LoadingView +import app.k9mail.core.ui.compose.designsystem.template.ResponsiveWidthContainer +import app.k9mail.core.ui.compose.theme.PreviewWithThemes +import app.k9mail.feature.account.common.ui.loadingerror.rememberContentLoadingErrorViewState +import app.k9mail.feature.account.common.ui.view.SuccessView +import app.k9mail.feature.account.edit.R + +@Composable +fun SaveServerSettingsContent( + state: SaveServerSettingsContract.State, + contentPadding: PaddingValues, + modifier: Modifier = Modifier, +) { + ResponsiveWidthContainer( + modifier = Modifier + .testTag("SaveServerSettingsContent") + .padding(contentPadding) + .then(modifier), + ) { + ContentLoadingErrorView( + state = rememberContentLoadingErrorViewState(state), + loading = { + LoadingView( + message = stringResource(id = R.string.account_edit_save_server_settings_loading_message), + ) + }, + error = { + ErrorView( + title = stringResource(id = R.string.account_edit_save_server_settings_error_message), + ) + }, + content = { + SuccessView( + message = stringResource(id = R.string.account_edit_save_server_settings_success_message), + ) + }, + modifier = Modifier.fillMaxSize(), + ) + } +} + +@Preview(showBackground = true) +@Composable +internal fun UpdateServerSettingsContentPreview() { + PreviewWithThemes { + SaveServerSettingsContent( + state = SaveServerSettingsContract.State( + isLoading = false, + error = null, + ), + contentPadding = PaddingValues(), + ) + } +} + +@Preview(showBackground = true) +@Composable +internal fun UpdateServerSettingsContentLoadingPreview() { + PreviewWithThemes { + SaveServerSettingsContent( + state = SaveServerSettingsContract.State( + isLoading = true, + error = null, + ), + + contentPadding = PaddingValues(), + ) + } +} + +@Preview(showBackground = true) +@Composable +internal fun UpdateServerSettingsContentErrorPreview() { + PreviewWithThemes { + SaveServerSettingsContent( + state = SaveServerSettingsContract.State( + isLoading = false, + error = SaveServerSettingsContract.Failure.SaveServerSettingsFailed( + message = "Error", + ), + ), + contentPadding = PaddingValues(), + ) + } +} diff --git a/feature/account/edit/src/main/kotlin/app/k9mail/feature/account/edit/ui/server/settings/save/SaveServerSettingsContract.kt b/feature/account/edit/src/main/kotlin/app/k9mail/feature/account/edit/ui/server/settings/save/SaveServerSettingsContract.kt new file mode 100644 index 00000000000..b1d19c6a9df --- /dev/null +++ b/feature/account/edit/src/main/kotlin/app/k9mail/feature/account/edit/ui/server/settings/save/SaveServerSettingsContract.kt @@ -0,0 +1,33 @@ +package app.k9mail.feature.account.edit.ui.server.settings.save + +import app.k9mail.core.ui.compose.common.mvi.UnidirectionalViewModel +import app.k9mail.feature.account.common.ui.loadingerror.LoadingErrorState + +interface SaveServerSettingsContract { + + interface ViewModel : UnidirectionalViewModel { + val isIncoming: Boolean + } + + data class State( + override val error: Failure? = null, + override val isLoading: Boolean = true, + ) : LoadingErrorState + + sealed interface Event { + data object SaveServerSettings : Event + data object OnNextClicked : Event + data object OnBackClicked : Event + } + + sealed interface Effect { + data object NavigateNext : Effect + data object NavigateBack : Effect + } + + sealed interface Failure { + data class SaveServerSettingsFailed( + val message: String, + ) : Failure + } +} diff --git a/feature/account/edit/src/main/kotlin/app/k9mail/feature/account/edit/ui/server/settings/save/SaveServerSettingsScreen.kt b/feature/account/edit/src/main/kotlin/app/k9mail/feature/account/edit/ui/server/settings/save/SaveServerSettingsScreen.kt new file mode 100644 index 00000000000..b5ba1c941b1 --- /dev/null +++ b/feature/account/edit/src/main/kotlin/app/k9mail/feature/account/edit/ui/server/settings/save/SaveServerSettingsScreen.kt @@ -0,0 +1,100 @@ +package app.k9mail.feature.account.edit.ui.server.settings.save + +import androidx.activity.compose.BackHandler +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import app.k9mail.core.ui.compose.common.mvi.observe +import app.k9mail.core.ui.compose.designsystem.template.Scaffold +import app.k9mail.core.ui.compose.theme.K9Theme +import app.k9mail.core.ui.compose.theme.ThunderbirdTheme +import app.k9mail.feature.account.common.ui.AccountTopAppBarWithBackButton +import app.k9mail.feature.account.common.ui.WizardNavigationBar +import app.k9mail.feature.account.common.ui.WizardNavigationBarState +import app.k9mail.feature.account.edit.ui.server.settings.save.SaveServerSettingsContract.Effect +import app.k9mail.feature.account.edit.ui.server.settings.save.SaveServerSettingsContract.Event +import app.k9mail.feature.account.edit.ui.server.settings.save.SaveServerSettingsContract.ViewModel +import app.k9mail.feature.account.edit.ui.server.settings.save.fake.FakeSaveServerSettingsViewModel + +@Composable +fun SaveServerSettingsScreen( + onNext: () -> Unit, + onBack: () -> Unit, + viewModel: ViewModel, + modifier: Modifier = Modifier, +) { + val (state, dispatch) = viewModel.observe { effect -> + when (effect) { + is Effect.NavigateNext -> onNext() + Effect.NavigateBack -> onBack() + } + } + + LaunchedEffect(key1 = Unit) { + dispatch(Event.SaveServerSettings) + } + + BackHandler { + dispatch(Event.OnBackClicked) + } + + Scaffold( + topBar = { + AccountTopAppBarWithBackButton( + title = "Edit Server Settings", + onBackClicked = { + dispatch(Event.OnBackClicked) + }, + ) + }, + bottomBar = { + WizardNavigationBar( + onNextClick = { + dispatch(Event.OnNextClicked) + }, + onBackClick = { + dispatch(Event.OnBackClicked) + }, + state = WizardNavigationBarState( + isNextEnabled = state.value.error == null && !state.value.isLoading, + isBackEnabled = state.value.error != null, + ), + ) + }, + modifier = modifier, + ) { innerPadding -> + SaveServerSettingsContent( + state = state.value, + contentPadding = innerPadding, + ) + } +} + +@Composable +@Preview(showBackground = true) +internal fun SaveServerSettingsScreenK9Preview() { + K9Theme { + SaveServerSettingsScreen( + onNext = {}, + onBack = {}, + viewModel = FakeSaveServerSettingsViewModel( + isIncoming = true, + ), + ) + } +} + +@Composable +@Preview(showBackground = true) +internal fun SaveServerSettingsScreenThunderbirdPreview() { + ThunderbirdTheme { + SaveServerSettingsScreen( + onNext = {}, + onBack = {}, + viewModel = FakeSaveServerSettingsViewModel( + isIncoming = true, + ), + ) + } +} diff --git a/feature/account/edit/src/main/kotlin/app/k9mail/feature/account/edit/ui/server/settings/save/fake/FakeSaveServerSettingsViewModel.kt b/feature/account/edit/src/main/kotlin/app/k9mail/feature/account/edit/ui/server/settings/save/fake/FakeSaveServerSettingsViewModel.kt new file mode 100644 index 00000000000..fa0a2da7c1c --- /dev/null +++ b/feature/account/edit/src/main/kotlin/app/k9mail/feature/account/edit/ui/server/settings/save/fake/FakeSaveServerSettingsViewModel.kt @@ -0,0 +1,23 @@ +package app.k9mail.feature.account.edit.ui.server.settings.save.fake + +import app.k9mail.core.ui.compose.common.mvi.BaseViewModel +import app.k9mail.feature.account.edit.ui.server.settings.save.SaveServerSettingsContract.Effect +import app.k9mail.feature.account.edit.ui.server.settings.save.SaveServerSettingsContract.Event +import app.k9mail.feature.account.edit.ui.server.settings.save.SaveServerSettingsContract.State +import app.k9mail.feature.account.edit.ui.server.settings.save.SaveServerSettingsContract.ViewModel + +class FakeSaveServerSettingsViewModel( + override val isIncoming: Boolean, + initialState: State = State(), +) : BaseViewModel(initialState), ViewModel { + + val events = mutableListOf() + + override fun event(event: Event) { + events.add(event) + } + + fun effect(effect: Effect) { + emitEffect(effect) + } +} diff --git a/feature/account/edit/src/main/res/values/strings.xml b/feature/account/edit/src/main/res/values/strings.xml new file mode 100644 index 00000000000..863e8da67bd --- /dev/null +++ b/feature/account/edit/src/main/res/values/strings.xml @@ -0,0 +1,6 @@ + + + Saving server settings in progress + Saving server settings failed + Saving server settings was successful + diff --git a/feature/account/edit/src/test/kotlin/app/k9mail/feature/account/edit/AccountEditModuleKtTest.kt b/feature/account/edit/src/test/kotlin/app/k9mail/feature/account/edit/AccountEditModuleKtTest.kt index 696854a4096..a0175448ab4 100644 --- a/feature/account/edit/src/test/kotlin/app/k9mail/feature/account/edit/AccountEditModuleKtTest.kt +++ b/feature/account/edit/src/test/kotlin/app/k9mail/feature/account/edit/AccountEditModuleKtTest.kt @@ -5,6 +5,7 @@ import app.k9mail.core.common.oauth.OAuthConfigurationFactory import app.k9mail.feature.account.common.AccountCommonExternalContract import app.k9mail.feature.account.common.domain.entity.AccountState import app.k9mail.feature.account.common.domain.entity.InteractionMode +import app.k9mail.feature.account.edit.ui.server.settings.save.SaveServerSettingsContract import app.k9mail.feature.account.server.certificate.ui.ServerCertificateErrorContract import app.k9mail.feature.account.server.settings.ui.incoming.IncomingServerSettingsContract import app.k9mail.feature.account.server.settings.ui.outgoing.OutgoingServerSettingsContract @@ -46,6 +47,7 @@ class AccountEditModuleKtTest : KoinTest { } } } + single { Mockito.mock() } } @Test @@ -59,6 +61,8 @@ class AccountEditModuleKtTest : KoinTest { ServerCertificateErrorContract.State::class, IncomingServerSettingsContract.State::class, OutgoingServerSettingsContract.State::class, + SaveServerSettingsContract.State::class, + AccountEditExternalContract.AccountServerSettingsUpdater::class, InteractionMode::class, ), ) diff --git a/feature/account/edit/src/test/kotlin/app/k9mail/feature/account/edit/domain/usecase/GetAccountStateTest.kt b/feature/account/edit/src/test/kotlin/app/k9mail/feature/account/edit/domain/usecase/GetAccountStateTest.kt new file mode 100644 index 00000000000..4147c70f14a --- /dev/null +++ b/feature/account/edit/src/test/kotlin/app/k9mail/feature/account/edit/domain/usecase/GetAccountStateTest.kt @@ -0,0 +1,90 @@ +package app.k9mail.feature.account.edit.domain.usecase + +import app.k9mail.feature.account.common.data.InMemoryAccountStateRepository +import app.k9mail.feature.account.common.domain.entity.AccountOptions +import app.k9mail.feature.account.common.domain.entity.AccountState +import app.k9mail.feature.account.common.domain.entity.AuthorizationState +import app.k9mail.feature.account.common.domain.entity.MailConnectionSecurity +import assertk.assertFailure +import assertk.assertThat +import assertk.assertions.isEqualTo +import assertk.assertions.isInstanceOf +import assertk.assertions.prop +import com.fsck.k9.mail.AuthType +import com.fsck.k9.mail.ServerSettings +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class GetAccountStateTest { + + @Test + fun `should get account state from repository`() = runTest { + val testSubject = GetAccountState( + accountStateRepository = InMemoryAccountStateRepository(state = ACCOUNT_STATE), + ) + + val result = testSubject.execute(ACCOUNT_UUID) + + assertThat(result).isEqualTo(ACCOUNT_STATE) + } + + @Test + fun `should throw exception WHEN account state repository contains state for different account uuid`() = runTest { + val testSubject = GetAccountState( + accountStateRepository = InMemoryAccountStateRepository( + state = ACCOUNT_STATE.copy(uuid = "differentAccountUuid"), + ), + ) + + assertFailure { + testSubject.execute(ACCOUNT_UUID) + }.isInstanceOf() + .prop(IllegalStateException::message) + .isEqualTo("Account state for $ACCOUNT_UUID not found") + } + + private companion object { + const val ACCOUNT_UUID = "accountUuid" + const val EMAIL_ADDRESS = "test@example.com" + val INCOMING_SERVER_SETTINGS = ServerSettings( + type = "imap", + host = "imap.example.com", + port = 993, + connectionSecurity = MailConnectionSecurity.SSL_TLS_REQUIRED, + authenticationType = AuthType.PLAIN, + username = "user", + password = "password", + clientCertificateAlias = null, + ) + val OUTGOING_SERVER_SETTINGS = ServerSettings( + type = "smtp", + host = "smtp.example.com", + port = 465, + connectionSecurity = MailConnectionSecurity.SSL_TLS_REQUIRED, + authenticationType = AuthType.PLAIN, + username = "user", + password = "password", + clientCertificateAlias = null, + ) + + val AUTHORIZATION_STATE = AuthorizationState("authorization state") + + val OPTIONS = AccountOptions( + accountName = "accountName", + displayName = "displayName", + emailSignature = null, + checkFrequencyInMinutes = 15, + messageDisplayCount = 25, + showNotification = true, + ) + + val ACCOUNT_STATE = AccountState( + uuid = ACCOUNT_UUID, + emailAddress = EMAIL_ADDRESS, + incomingServerSettings = INCOMING_SERVER_SETTINGS, + outgoingServerSettings = OUTGOING_SERVER_SETTINGS, + authorizationState = AUTHORIZATION_STATE, + options = OPTIONS, + ) + } +} diff --git a/feature/account/edit/src/test/kotlin/app/k9mail/feature/account/edit/domain/usecase/LoadAccountStateTest.kt b/feature/account/edit/src/test/kotlin/app/k9mail/feature/account/edit/domain/usecase/LoadAccountStateTest.kt index 9199820695a..b302a934ca4 100644 --- a/feature/account/edit/src/test/kotlin/app/k9mail/feature/account/edit/domain/usecase/LoadAccountStateTest.kt +++ b/feature/account/edit/src/test/kotlin/app/k9mail/feature/account/edit/domain/usecase/LoadAccountStateTest.kt @@ -5,67 +5,46 @@ import app.k9mail.feature.account.common.domain.entity.AccountOptions import app.k9mail.feature.account.common.domain.entity.AccountState import app.k9mail.feature.account.common.domain.entity.AuthorizationState import app.k9mail.feature.account.common.domain.entity.MailConnectionSecurity +import assertk.assertFailure import assertk.assertThat import assertk.assertions.isEqualTo +import assertk.assertions.isInstanceOf +import assertk.assertions.prop import com.fsck.k9.mail.AuthType import com.fsck.k9.mail.ServerSettings -import kotlin.test.DefaultAsserter.fail import kotlinx.coroutines.test.runTest import org.junit.Test class LoadAccountStateTest { @Test - fun `should load account state WHEN account in state repository has different UUID`() = runTest { + fun `should load account state and update account state repository`() = runTest { + val accountStateRepository = InMemoryAccountStateRepository() val testSubject = LoadAccountState( accountStateLoader = { _ -> ACCOUNT_STATE }, - accountStateRepository = InMemoryAccountStateRepository( - state = ACCOUNT_STATE.copy(uuid = "differentUuid"), - ), + accountStateRepository = accountStateRepository, ) val result = testSubject.execute(ACCOUNT_UUID) assertThat(result).isEqualTo(ACCOUNT_STATE) + assertThat(accountStateRepository.getState()).isEqualTo(ACCOUNT_STATE) } @Test - fun `should return account state WHEN account in state repository has same UUID`() = runTest { - val testSubject = LoadAccountState( - accountStateLoader = { _ -> - fail("AccountStateLoader should not be called in this test") - }, - accountStateRepository = InMemoryAccountStateRepository( - state = ACCOUNT_STATE, - ), - ) - - val result = testSubject.execute(ACCOUNT_UUID) - - assertThat(result).isEqualTo(ACCOUNT_STATE) - } - - @Test - fun `should return empty account state WHEN account loader returns null`() = runTest { + fun `should throw exception WHEN account loader returns null`() = runTest { val testSubject = LoadAccountState( accountStateLoader = { null }, accountStateRepository = InMemoryAccountStateRepository(), ) - val result = testSubject.execute(ACCOUNT_UUID) - - assertThat(result).isEqualTo( - AccountState( - uuid = ACCOUNT_UUID, - emailAddress = null, - incomingServerSettings = null, - outgoingServerSettings = null, - authorizationState = null, - options = null, - ), - ) + assertFailure { + testSubject.execute(ACCOUNT_UUID) + }.isInstanceOf() + .prop(IllegalStateException::message) + .isEqualTo("Account state for $ACCOUNT_UUID not found") } private companion object { diff --git a/feature/account/edit/src/test/kotlin/app/k9mail/feature/account/edit/domain/usecase/SaveServerSettingsTest.kt b/feature/account/edit/src/test/kotlin/app/k9mail/feature/account/edit/domain/usecase/SaveServerSettingsTest.kt new file mode 100644 index 00000000000..70b1df3c0ea --- /dev/null +++ b/feature/account/edit/src/test/kotlin/app/k9mail/feature/account/edit/domain/usecase/SaveServerSettingsTest.kt @@ -0,0 +1,161 @@ +package app.k9mail.feature.account.edit.domain.usecase + +import app.k9mail.feature.account.common.domain.entity.AccountOptions +import app.k9mail.feature.account.common.domain.entity.AccountState +import app.k9mail.feature.account.common.domain.entity.AuthorizationState +import app.k9mail.feature.account.common.domain.entity.MailConnectionSecurity +import app.k9mail.feature.account.edit.AccountEditExternalContract.AccountUpdaterFailure +import app.k9mail.feature.account.edit.AccountEditExternalContract.AccountUpdaterResult +import assertk.assertFailure +import assertk.assertThat +import assertk.assertions.isEqualTo +import assertk.assertions.isInstanceOf +import assertk.assertions.prop +import com.fsck.k9.mail.AuthType +import com.fsck.k9.mail.ServerSettings +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class SaveServerSettingsTest { + + @Test + fun `should get account state and update incoming server settings`() = runTest { + var recordedAccountUuid: String? = null + var recordedIsIncoming: Boolean? = null + var recordedServerSettings: ServerSettings? = null + val testSubject = SaveServerSettings( + getAccountState = { _ -> ACCOUNT_STATE }, + serverSettingsUpdater = { accountUuid, isIncoming, serverSettings -> + recordedAccountUuid = accountUuid + recordedIsIncoming = isIncoming + recordedServerSettings = serverSettings + + AccountUpdaterResult.Success(accountUuid) + }, + ) + + testSubject.execute(ACCOUNT_UUID, isIncoming = true) + + assertThat(recordedAccountUuid).isEqualTo(ACCOUNT_UUID) + assertThat(recordedIsIncoming).isEqualTo(true) + assertThat(recordedServerSettings).isEqualTo(INCOMING_SERVER_SETTINGS) + } + + @Test + fun `should throw exception WHEN no incoming server settings present`() = runTest { + val testSubject = SaveServerSettings( + getAccountState = { _ -> ACCOUNT_STATE.copy(incomingServerSettings = null) }, + serverSettingsUpdater = { accountUuid, _, _ -> + AccountUpdaterResult.Success(accountUuid) + }, + ) + + assertFailure { + testSubject.execute(ACCOUNT_UUID, isIncoming = true) + }.isInstanceOf() + .prop(IllegalStateException::message) + .isEqualTo("Server settings not found") + } + + @Test + fun `should get account state and update outgoing server settings`() = runTest { + var recordedAccountUuid: String? = null + var recordedIsIncoming: Boolean? = null + var recordedServerSettings: ServerSettings? = null + val testSubject = SaveServerSettings( + getAccountState = { _ -> ACCOUNT_STATE }, + serverSettingsUpdater = { accountUuid, isIncoming, serverSettings -> + recordedAccountUuid = accountUuid + recordedIsIncoming = isIncoming + recordedServerSettings = serverSettings + + AccountUpdaterResult.Success(accountUuid) + }, + ) + + testSubject.execute(ACCOUNT_UUID, isIncoming = false) + + assertThat(recordedAccountUuid).isEqualTo(ACCOUNT_UUID) + assertThat(recordedIsIncoming).isEqualTo(false) + assertThat(recordedServerSettings).isEqualTo(OUTGOING_SERVER_SETTINGS) + } + + @Test + fun `should throw exception WHEN no outgoing server settings present`() = runTest { + val testSubject = SaveServerSettings( + getAccountState = { _ -> ACCOUNT_STATE.copy(outgoingServerSettings = null) }, + serverSettingsUpdater = { accountUuid, _, _ -> + AccountUpdaterResult.Success(accountUuid) + }, + ) + + assertFailure { + testSubject.execute(ACCOUNT_UUID, isIncoming = false) + }.isInstanceOf() + .prop(IllegalStateException::message) + .isEqualTo("Server settings not found") + } + + @Test + fun `should throw exception WHEN update failed`() = runTest { + val testSubject = SaveServerSettings( + getAccountState = { _ -> ACCOUNT_STATE }, + serverSettingsUpdater = { _, _, _ -> + AccountUpdaterResult.Failure( + AccountUpdaterFailure.AccountNotFound(ACCOUNT_UUID), + ) + }, + ) + + assertFailure { + testSubject.execute(ACCOUNT_UUID, isIncoming = true) + }.isInstanceOf() + .prop(IllegalStateException::message) + .isEqualTo("Server settings update failed") + } + + private companion object { + const val ACCOUNT_UUID = "accountUuid" + const val EMAIL_ADDRESS = "test@example.com" + val INCOMING_SERVER_SETTINGS = ServerSettings( + type = "imap", + host = "imap.example.com", + port = 993, + connectionSecurity = MailConnectionSecurity.SSL_TLS_REQUIRED, + authenticationType = AuthType.PLAIN, + username = "user", + password = "password", + clientCertificateAlias = null, + ) + val OUTGOING_SERVER_SETTINGS = ServerSettings( + type = "smtp", + host = "smtp.example.com", + port = 465, + connectionSecurity = MailConnectionSecurity.SSL_TLS_REQUIRED, + authenticationType = AuthType.PLAIN, + username = "user", + password = "password", + clientCertificateAlias = null, + ) + + val AUTHORIZATION_STATE = AuthorizationState("authorization state") + + val OPTIONS = AccountOptions( + accountName = "accountName", + displayName = "displayName", + emailSignature = null, + checkFrequencyInMinutes = 15, + messageDisplayCount = 25, + showNotification = true, + ) + + val ACCOUNT_STATE = AccountState( + uuid = ACCOUNT_UUID, + emailAddress = EMAIL_ADDRESS, + incomingServerSettings = INCOMING_SERVER_SETTINGS, + outgoingServerSettings = OUTGOING_SERVER_SETTINGS, + authorizationState = AUTHORIZATION_STATE, + options = OPTIONS, + ) + } +} diff --git a/feature/account/edit/src/test/kotlin/app/k9mail/feature/account/edit/ui/EditIncomingServerSettingsViewModelTest.kt b/feature/account/edit/src/test/kotlin/app/k9mail/feature/account/edit/ui/server/settings/modify/ModifyIncomingServerSettingsViewModelTest.kt similarity index 81% rename from feature/account/edit/src/test/kotlin/app/k9mail/feature/account/edit/ui/EditIncomingServerSettingsViewModelTest.kt rename to feature/account/edit/src/test/kotlin/app/k9mail/feature/account/edit/ui/server/settings/modify/ModifyIncomingServerSettingsViewModelTest.kt index be2c90395a2..3c2f668fa35 100644 --- a/feature/account/edit/src/test/kotlin/app/k9mail/feature/account/edit/ui/EditIncomingServerSettingsViewModelTest.kt +++ b/feature/account/edit/src/test/kotlin/app/k9mail/feature/account/edit/ui/server/settings/modify/ModifyIncomingServerSettingsViewModelTest.kt @@ -1,4 +1,4 @@ -package app.k9mail.feature.account.edit.ui +package app.k9mail.feature.account.edit.ui.server.settings.modify import app.k9mail.core.ui.compose.testing.MainDispatcherRule import app.k9mail.core.ui.compose.testing.mvi.assertThatAndMviTurbinesConsumed @@ -16,12 +16,13 @@ import app.k9mail.feature.account.server.settings.ui.incoming.fake.FakeIncomingS import assertk.assertions.isEqualTo import com.fsck.k9.mail.AuthType import com.fsck.k9.mail.ServerSettings +import com.fsck.k9.mail.store.imap.ImapStoreSettings import kotlinx.coroutines.delay import kotlinx.coroutines.test.runTest import org.junit.Rule import org.junit.Test -class EditIncomingServerSettingsViewModelTest { +class ModifyIncomingServerSettingsViewModelTest { @get:Rule val mainDispatcherRule = MainDispatcherRule() @@ -41,11 +42,16 @@ class EditIncomingServerSettingsViewModelTest { "username", "password", clientCertificateAlias = null, - extra = emptyMap(), + extra = ImapStoreSettings.createExtra( + autoDetectNamespace = true, + pathPrefix = null, + useCompression = true, + sendClientId = true, + ), ), ) - val testSubject = EditIncomingServerSettingsViewModel( + val testSubject = ModifyIncomingServerSettingsViewModel( accountUuid = accountUuid, accountStateLoader = { _ -> delay(50) @@ -71,6 +77,10 @@ class EditIncomingServerSettingsViewModelTest { authenticationType = AuthenticationType.PasswordCleartext, username = StringInputField(value = "username"), password = StringInputField(value = "password"), + imapAutodetectNamespaceEnabled = true, + imapPrefix = StringInputField(value = ""), + imapUseCompression = true, + imapSendClientId = true, ), ) } diff --git a/feature/account/edit/src/test/kotlin/app/k9mail/feature/account/edit/ui/EditOutgoingServerSettingsViewModelTest.kt b/feature/account/edit/src/test/kotlin/app/k9mail/feature/account/edit/ui/server/settings/modify/ModifyOutgoingServerSettingsViewModelTest.kt similarity index 94% rename from feature/account/edit/src/test/kotlin/app/k9mail/feature/account/edit/ui/EditOutgoingServerSettingsViewModelTest.kt rename to feature/account/edit/src/test/kotlin/app/k9mail/feature/account/edit/ui/server/settings/modify/ModifyOutgoingServerSettingsViewModelTest.kt index 2ebfd45d58f..483a1038b86 100644 --- a/feature/account/edit/src/test/kotlin/app/k9mail/feature/account/edit/ui/EditOutgoingServerSettingsViewModelTest.kt +++ b/feature/account/edit/src/test/kotlin/app/k9mail/feature/account/edit/ui/server/settings/modify/ModifyOutgoingServerSettingsViewModelTest.kt @@ -1,4 +1,4 @@ -package app.k9mail.feature.account.edit.ui +package app.k9mail.feature.account.edit.ui.server.settings.modify import app.k9mail.core.ui.compose.testing.MainDispatcherRule import app.k9mail.core.ui.compose.testing.mvi.assertThatAndMviTurbinesConsumed @@ -21,7 +21,7 @@ import kotlinx.coroutines.test.runTest import org.junit.Rule import org.junit.Test -class EditOutgoingServerSettingsViewModelTest { +class ModifyOutgoingServerSettingsViewModelTest { @get:Rule val mainDispatcherRule = MainDispatcherRule() @@ -44,7 +44,7 @@ class EditOutgoingServerSettingsViewModelTest { extra = emptyMap(), ), ) - val testSubject = EditOutgoingServerSettingsViewModel( + val testSubject = ModifyOutgoingServerSettingsViewModel( accountUuid = accountUuid, accountStateLoader = { _ -> delay(50) diff --git a/feature/account/edit/src/test/kotlin/app/k9mail/feature/account/edit/ui/server/settings/save/BaseSaveServerSettingsViewModelTest.kt b/feature/account/edit/src/test/kotlin/app/k9mail/feature/account/edit/ui/server/settings/save/BaseSaveServerSettingsViewModelTest.kt new file mode 100644 index 00000000000..85bde1bbf20 --- /dev/null +++ b/feature/account/edit/src/test/kotlin/app/k9mail/feature/account/edit/ui/server/settings/save/BaseSaveServerSettingsViewModelTest.kt @@ -0,0 +1,155 @@ +package app.k9mail.feature.account.edit.ui.server.settings.save + +import app.k9mail.core.ui.compose.testing.MainDispatcherRule +import app.k9mail.core.ui.compose.testing.mvi.assertThatAndEffectTurbineConsumed +import app.k9mail.core.ui.compose.testing.mvi.assertThatAndStateTurbineConsumed +import app.k9mail.core.ui.compose.testing.mvi.turbinesWithInitialStateCheck +import app.k9mail.feature.account.edit.domain.AccountEditDomainContract +import app.k9mail.feature.account.edit.ui.server.settings.save.SaveServerSettingsContract.Effect +import app.k9mail.feature.account.edit.ui.server.settings.save.SaveServerSettingsContract.Event +import app.k9mail.feature.account.edit.ui.server.settings.save.SaveServerSettingsContract.Failure +import app.k9mail.feature.account.edit.ui.server.settings.save.SaveServerSettingsContract.State +import assertk.assertThat +import assertk.assertions.isEqualTo +import assertk.assertions.isNotNull +import kotlinx.coroutines.test.runTest +import org.junit.Rule +import org.junit.Test + +class BaseSaveServerSettingsViewModelTest { + + @get:Rule + val mainDispatcherRule = MainDispatcherRule() + + @Test + fun `should save server settings when SaveServerSettings event received and emit NavigateNext`() = runTest { + var recordedAccountUuid: String? = null + var recordedIsIncoming: Boolean? = null + val testSubject = TestSaveServerSettingsViewModel( + accountUuid = ACCOUNT_UUID, + saveServerSettings = { accountUuid, isIncoming -> + recordedAccountUuid = accountUuid + recordedIsIncoming = isIncoming + }, + ) + val turbines = turbinesWithInitialStateCheck(testSubject, State()) + + testSubject.event(Event.SaveServerSettings) + + turbines.assertThatAndStateTurbineConsumed { + isEqualTo(State(isLoading = false)) + } + + assertThat(recordedAccountUuid).isNotNull().isEqualTo(ACCOUNT_UUID) + assertThat(recordedIsIncoming).isNotNull().isEqualTo(true) + + turbines.assertThatAndEffectTurbineConsumed { + isEqualTo(Effect.NavigateNext) + } + } + + @Test + fun `should set error state when save settings failed`() = runTest { + val testSubject = TestSaveServerSettingsViewModel( + accountUuid = ACCOUNT_UUID, + saveServerSettings = { _, _ -> + error("Test exception") + }, + ) + val turbines = turbinesWithInitialStateCheck(testSubject, State()) + + testSubject.event(Event.SaveServerSettings) + + turbines.assertThatAndStateTurbineConsumed { + isEqualTo( + State( + error = Failure.SaveServerSettingsFailed("Test exception"), + isLoading = false, + ), + ) + } + } + + @Test + fun `should prevent navigation effects when in loading state`() = runTest { + val testSubject = TestSaveServerSettingsViewModel( + accountUuid = ACCOUNT_UUID, + saveServerSettings = { _, _ -> + // Do nothing + }, + initialState = State(isLoading = true), + ) + val turbines = turbinesWithInitialStateCheck(testSubject, State(isLoading = true)) + + testSubject.event(Event.OnNextClicked) + + turbines.effectTurbine.ensureAllEventsConsumed() + + testSubject.event(Event.OnBackClicked) + + turbines.effectTurbine.ensureAllEventsConsumed() + } + + @Test + fun `should allow NavigateNext when no error and not loading`() = runTest { + val testSubject = TestSaveServerSettingsViewModel( + accountUuid = ACCOUNT_UUID, + saveServerSettings = { _, _ -> + // Do nothing + }, + initialState = State(isLoading = false), + ) + val turbines = turbinesWithInitialStateCheck(testSubject, State(isLoading = false)) + + testSubject.event(Event.OnNextClicked) + + turbines.assertThatAndEffectTurbineConsumed { + isEqualTo(Effect.NavigateNext) + } + + testSubject.event(Event.OnBackClicked) + + turbines.effectTurbine.ensureAllEventsConsumed() + } + + @Test + fun `should allow NavigateBack when error and not loading`() = runTest { + val failure = Failure.SaveServerSettingsFailed("Test exception") + val testSubject = TestSaveServerSettingsViewModel( + accountUuid = ACCOUNT_UUID, + saveServerSettings = { _, _ -> + // Do nothing + }, + initialState = State( + isLoading = false, + error = failure, + ), + ) + val turbines = turbinesWithInitialStateCheck(testSubject, State(isLoading = false, error = failure)) + + testSubject.event(Event.OnNextClicked) + + turbines.effectTurbine.ensureAllEventsConsumed() + + testSubject.event(Event.OnBackClicked) + + turbines.assertThatAndEffectTurbineConsumed { + isEqualTo(Effect.NavigateBack) + } + } + + private class TestSaveServerSettingsViewModel( + accountUuid: String, + saveServerSettings: AccountEditDomainContract.UseCase.SaveServerSettings, + initialState: State = State(), + ) : BaseSaveServerSettingsViewModel( + accountUuid = accountUuid, + isIncoming = true, + saveServerSettings = saveServerSettings, + initialState = initialState, + ) + + private companion object { + const val ACCOUNT_UUID = "accountUuid" + } +} diff --git a/feature/account/edit/src/test/kotlin/app/k9mail/feature/account/edit/ui/server/settings/save/SaveIncomingServerSettingsViewModelTest.kt b/feature/account/edit/src/test/kotlin/app/k9mail/feature/account/edit/ui/server/settings/save/SaveIncomingServerSettingsViewModelTest.kt new file mode 100644 index 00000000000..dd44ba2b92e --- /dev/null +++ b/feature/account/edit/src/test/kotlin/app/k9mail/feature/account/edit/ui/server/settings/save/SaveIncomingServerSettingsViewModelTest.kt @@ -0,0 +1,18 @@ +package app.k9mail.feature.account.edit.ui.server.settings.save + +import assertk.assertThat +import assertk.assertions.isTrue +import org.junit.Test + +class SaveIncomingServerSettingsViewModelTest { + + @Test + fun `should set is incoming to true`() { + val testSubject = SaveIncomingServerSettingsViewModel( + accountUuid = "accountUuid", + saveServerSettings = { _, _ -> }, + ) + + assertThat(testSubject.isIncoming).isTrue() + } +} diff --git a/feature/account/edit/src/test/kotlin/app/k9mail/feature/account/edit/ui/server/settings/save/SaveOutgoingServerSettingsViewModelTest.kt b/feature/account/edit/src/test/kotlin/app/k9mail/feature/account/edit/ui/server/settings/save/SaveOutgoingServerSettingsViewModelTest.kt new file mode 100644 index 00000000000..7f3205ca637 --- /dev/null +++ b/feature/account/edit/src/test/kotlin/app/k9mail/feature/account/edit/ui/server/settings/save/SaveOutgoingServerSettingsViewModelTest.kt @@ -0,0 +1,18 @@ +package app.k9mail.feature.account.edit.ui.server.settings.save + +import assertk.assertThat +import assertk.assertions.isFalse +import org.junit.Test + +class SaveOutgoingServerSettingsViewModelTest { + + @Test + fun `should set is incoming to true`() { + val testSubject = SaveOutgoingServerSettingsViewModel( + accountUuid = "accountUuid", + saveServerSettings = { _, _ -> }, + ) + + assertThat(testSubject.isIncoming).isFalse() + } +} diff --git a/feature/account/edit/src/test/kotlin/app/k9mail/feature/account/edit/ui/server/settings/save/SaveServerSettingsScreenKtTest.kt b/feature/account/edit/src/test/kotlin/app/k9mail/feature/account/edit/ui/server/settings/save/SaveServerSettingsScreenKtTest.kt new file mode 100644 index 00000000000..2fa19952799 --- /dev/null +++ b/feature/account/edit/src/test/kotlin/app/k9mail/feature/account/edit/ui/server/settings/save/SaveServerSettingsScreenKtTest.kt @@ -0,0 +1,49 @@ +package app.k9mail.feature.account.edit.ui.server.settings.save + +import app.k9mail.core.ui.compose.testing.ComposeTest +import app.k9mail.core.ui.compose.testing.setContent +import app.k9mail.feature.account.edit.ui.server.settings.save.SaveServerSettingsContract.Effect +import app.k9mail.feature.account.edit.ui.server.settings.save.SaveServerSettingsContract.State +import app.k9mail.feature.account.edit.ui.server.settings.save.fake.FakeSaveServerSettingsViewModel +import assertk.assertThat +import assertk.assertions.isEqualTo +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class SaveServerSettingsScreenKtTest : ComposeTest() { + + @Test + fun `should delegate navigation effects`() = runTest { + val initialState = State( + isLoading = false, + error = null, + ) + val viewModel = FakeSaveServerSettingsViewModel( + isIncoming = true, + initialState = initialState, + ) + var onNextCounter = 0 + var onBackCounter = 0 + + setContent { + SaveServerSettingsScreen( + onNext = { onNextCounter++ }, + onBack = { onBackCounter++ }, + viewModel = viewModel, + ) + } + + assertThat(onNextCounter).isEqualTo(0) + assertThat(onBackCounter).isEqualTo(0) + + viewModel.effect(Effect.NavigateNext) + + assertThat(onNextCounter).isEqualTo(1) + assertThat(onBackCounter).isEqualTo(0) + + viewModel.effect(Effect.NavigateBack) + + assertThat(onNextCounter).isEqualTo(1) + assertThat(onBackCounter).isEqualTo(1) + } +} diff --git a/feature/account/edit/src/test/kotlin/app/k9mail/feature/account/edit/ui/server/settings/save/SaveServerSettingsStateTest.kt b/feature/account/edit/src/test/kotlin/app/k9mail/feature/account/edit/ui/server/settings/save/SaveServerSettingsStateTest.kt new file mode 100644 index 00000000000..5411ea30f47 --- /dev/null +++ b/feature/account/edit/src/test/kotlin/app/k9mail/feature/account/edit/ui/server/settings/save/SaveServerSettingsStateTest.kt @@ -0,0 +1,21 @@ +package app.k9mail.feature.account.edit.ui.server.settings.save + +import app.k9mail.feature.account.edit.ui.server.settings.save.SaveServerSettingsContract.State +import assertk.assertThat +import assertk.assertions.isEqualTo +import org.junit.Test + +class SaveServerSettingsStateTest { + + @Test + fun `should set default values`() { + val state = State() + + assertThat(state).isEqualTo( + State( + error = null, + isLoading = true, + ), + ) + } +} diff --git a/feature/account/server/settings/src/main/kotlin/app/k9mail/feature/account/server/settings/ui/incoming/IncomingServerSettingsStateMapper.kt b/feature/account/server/settings/src/main/kotlin/app/k9mail/feature/account/server/settings/ui/incoming/IncomingServerSettingsStateMapper.kt index 2fef2862ae5..3c33e3df570 100644 --- a/feature/account/server/settings/src/main/kotlin/app/k9mail/feature/account/server/settings/ui/incoming/IncomingServerSettingsStateMapper.kt +++ b/feature/account/server/settings/src/main/kotlin/app/k9mail/feature/account/server/settings/ui/incoming/IncomingServerSettingsStateMapper.kt @@ -11,24 +11,29 @@ import app.k9mail.feature.account.common.domain.input.StringInputField import app.k9mail.feature.account.server.settings.ui.incoming.IncomingServerSettingsContract.State import com.fsck.k9.mail.ServerSettings import com.fsck.k9.mail.store.imap.ImapStoreSettings +import com.fsck.k9.mail.store.imap.ImapStoreSettings.autoDetectNamespace +import com.fsck.k9.mail.store.imap.ImapStoreSettings.isSendClientId +import com.fsck.k9.mail.store.imap.ImapStoreSettings.isUseCompression +import com.fsck.k9.mail.store.imap.ImapStoreSettings.pathPrefix -fun AccountState.toIncomingConfigState(): State { - val incomingServerSettings = incomingServerSettings - return if (incomingServerSettings == null) { - State( - username = StringInputField(value = emailAddress ?: ""), - ) - } else { - State( - protocolType = IncomingProtocolType.fromName(incomingServerSettings.type), - server = StringInputField(value = incomingServerSettings.host ?: ""), - security = incomingServerSettings.connectionSecurity.toConnectionSecurity(), - port = NumberInputField(value = incomingServerSettings.port.toLong()), - authenticationType = incomingServerSettings.authenticationType.toAuthenticationType(), - username = StringInputField(value = incomingServerSettings.username), - password = StringInputField(value = incomingServerSettings.password ?: ""), - ) - } +fun AccountState.toIncomingServerSettingsState() = incomingServerSettings?.toIncomingServerSettingsState() + ?: State(username = StringInputField(value = emailAddress ?: "")) + +private fun ServerSettings.toIncomingServerSettingsState(): State { + return State( + protocolType = IncomingProtocolType.fromName(type), + server = StringInputField(value = host ?: ""), + security = connectionSecurity.toConnectionSecurity(), + port = NumberInputField(value = port.toLong()), + authenticationType = authenticationType.toAuthenticationType(), + username = StringInputField(value = username), + password = StringInputField(value = password ?: ""), + clientCertificateAlias = clientCertificateAlias, + imapAutodetectNamespaceEnabled = autoDetectNamespace, + imapPrefix = StringInputField(value = pathPrefix ?: ""), + imapUseCompression = isUseCompression, + imapSendClientId = isSendClientId, + ) } internal fun State.toServerSettings(): ServerSettings { diff --git a/feature/account/server/settings/src/main/kotlin/app/k9mail/feature/account/server/settings/ui/incoming/IncomingServerSettingsViewModel.kt b/feature/account/server/settings/src/main/kotlin/app/k9mail/feature/account/server/settings/ui/incoming/IncomingServerSettingsViewModel.kt index c0560fb0e03..cc6e084d279 100644 --- a/feature/account/server/settings/src/main/kotlin/app/k9mail/feature/account/server/settings/ui/incoming/IncomingServerSettingsViewModel.kt +++ b/feature/account/server/settings/src/main/kotlin/app/k9mail/feature/account/server/settings/ui/incoming/IncomingServerSettingsViewModel.kt @@ -54,7 +54,7 @@ open class IncomingServerSettingsViewModel( protected open fun loadAccountState() { updateState { - accountStateRepository.getState().toIncomingConfigState() + accountStateRepository.getState().toIncomingServerSettingsState() } } diff --git a/feature/account/server/settings/src/main/kotlin/app/k9mail/feature/account/server/settings/ui/outgoing/OutgoingServerSettingsStateMapper.kt b/feature/account/server/settings/src/main/kotlin/app/k9mail/feature/account/server/settings/ui/outgoing/OutgoingServerSettingsStateMapper.kt index 8543f559622..e33ff408d25 100644 --- a/feature/account/server/settings/src/main/kotlin/app/k9mail/feature/account/server/settings/ui/outgoing/OutgoingServerSettingsStateMapper.kt +++ b/feature/account/server/settings/src/main/kotlin/app/k9mail/feature/account/server/settings/ui/outgoing/OutgoingServerSettingsStateMapper.kt @@ -10,22 +10,18 @@ import app.k9mail.feature.account.common.domain.input.StringInputField import app.k9mail.feature.account.server.settings.ui.outgoing.OutgoingServerSettingsContract.State import com.fsck.k9.mail.ServerSettings -fun AccountState.toOutgoingConfigState(): State { - val outgoingServerSettings = outgoingServerSettings - return if (outgoingServerSettings == null) { - State( - username = StringInputField(value = emailAddress ?: ""), - ) - } else { - State( - server = StringInputField(value = outgoingServerSettings.host ?: ""), - security = outgoingServerSettings.connectionSecurity.toConnectionSecurity(), - port = NumberInputField(value = outgoingServerSettings.port.toLong()), - authenticationType = outgoingServerSettings.authenticationType.toAuthenticationType(), - username = StringInputField(value = outgoingServerSettings.username), - password = StringInputField(value = outgoingServerSettings.password ?: ""), - ) - } +fun AccountState.toOutgoingServerSettingsState() = outgoingServerSettings?.toOutgoingServerSettingsState() + ?: State(username = StringInputField(value = emailAddress ?: "")) + +private fun ServerSettings.toOutgoingServerSettingsState(): State { + return State( + server = StringInputField(value = host ?: ""), + security = connectionSecurity.toConnectionSecurity(), + port = NumberInputField(value = port.toLong()), + authenticationType = authenticationType.toAuthenticationType(), + username = StringInputField(value = username), + password = StringInputField(value = password ?: ""), + ) } internal fun State.toServerSettings(): ServerSettings { diff --git a/feature/account/server/settings/src/main/kotlin/app/k9mail/feature/account/server/settings/ui/outgoing/OutgoingServerSettingsViewModel.kt b/feature/account/server/settings/src/main/kotlin/app/k9mail/feature/account/server/settings/ui/outgoing/OutgoingServerSettingsViewModel.kt index b1dd4566eb7..5e3ef856a60 100644 --- a/feature/account/server/settings/src/main/kotlin/app/k9mail/feature/account/server/settings/ui/outgoing/OutgoingServerSettingsViewModel.kt +++ b/feature/account/server/settings/src/main/kotlin/app/k9mail/feature/account/server/settings/ui/outgoing/OutgoingServerSettingsViewModel.kt @@ -40,7 +40,7 @@ open class OutgoingServerSettingsViewModel( protected open fun loadAccountState() { updateState { - accountStateRepository.getState().toOutgoingConfigState() + accountStateRepository.getState().toOutgoingServerSettingsState() } } diff --git a/feature/account/server/settings/src/test/kotlin/app/k9mail/feature/account/server/settings/ui/incoming/IncomingServerSettingsStateMapperKtTest.kt b/feature/account/server/settings/src/test/kotlin/app/k9mail/feature/account/server/settings/ui/incoming/IncomingServerSettingsStateMapperKtTest.kt index 31c48ba61c7..91deb5ec6fb 100644 --- a/feature/account/server/settings/src/test/kotlin/app/k9mail/feature/account/server/settings/ui/incoming/IncomingServerSettingsStateMapperKtTest.kt +++ b/feature/account/server/settings/src/test/kotlin/app/k9mail/feature/account/server/settings/ui/incoming/IncomingServerSettingsStateMapperKtTest.kt @@ -1,5 +1,6 @@ package app.k9mail.feature.account.server.settings.ui.incoming +import app.k9mail.feature.account.common.domain.entity.AccountState import app.k9mail.feature.account.common.domain.entity.AuthenticationType import app.k9mail.feature.account.common.domain.entity.ConnectionSecurity import app.k9mail.feature.account.common.domain.entity.IncomingProtocolType @@ -17,8 +18,48 @@ import org.junit.Test class IncomingServerSettingsStateMapperKtTest { @Test - fun `should map to IMAP server settings`() { - val incomingState = State( + fun `should map to state with email as username when server settings are null`() { + val accountState = AccountState( + emailAddress = "test@example.com", + incomingServerSettings = null, + ) + + val result = accountState.toIncomingServerSettingsState() + + assertThat(result).isEqualTo(State(username = StringInputField(value = "test@example.com"))) + } + + @Test + fun `should map from IMAP server settings to state`() { + val serverSettings = AccountState( + incomingServerSettings = IMAP_SERVER_SETTINGS, + ) + + val result = serverSettings.toIncomingServerSettingsState() + + assertThat(result).isEqualTo(INCOMING_IMAP_STATE) + } + + @Test + fun `should map from state to IMAP server settings`() { + val incomingState = INCOMING_IMAP_STATE + + val result = incomingState.toServerSettings() + + assertThat(result).isEqualTo(IMAP_SERVER_SETTINGS) + } + + @Test + fun `should map from state to POP3 server settings`() { + val incomingState = INCOMING_POP3_STATE + + val result = incomingState.toServerSettings() + + assertThat(result).isEqualTo(POP3_SERVER_SETTINGS) + } + + private companion object { + private val INCOMING_IMAP_STATE = State( protocolType = IncomingProtocolType.IMAP, server = StringInputField(value = "imap.example.org"), port = NumberInputField(value = 993), @@ -27,64 +68,49 @@ class IncomingServerSettingsStateMapperKtTest { username = StringInputField(value = "user"), password = StringInputField(value = "password"), clientCertificateAlias = null, - imapAutodetectNamespaceEnabled = true, + imapAutodetectNamespaceEnabled = false, imapPrefix = StringInputField(value = "prefix"), imapUseCompression = true, imapSendClientId = true, ) - val result = incomingState.toServerSettings() - - assertThat(result).isEqualTo( - ServerSettings( - type = "imap", - host = "imap.example.org", - port = 993, - connectionSecurity = MailConnectionSecurity.SSL_TLS_REQUIRED, - authenticationType = AuthType.PLAIN, - username = "user", - password = "password", - clientCertificateAlias = null, - extra = ImapStoreSettings.createExtra( - autoDetectNamespace = true, - pathPrefix = null, - useCompression = true, - sendClientId = true, - ), + private val IMAP_SERVER_SETTINGS = ServerSettings( + type = "imap", + host = "imap.example.org", + port = 993, + connectionSecurity = MailConnectionSecurity.SSL_TLS_REQUIRED, + authenticationType = AuthType.PLAIN, + username = "user", + password = "password", + clientCertificateAlias = null, + extra = ImapStoreSettings.createExtra( + autoDetectNamespace = false, + pathPrefix = "prefix", + useCompression = true, + sendClientId = true, ), ) - } - @Test - fun `should map to POP3 server settings`() { - val incomingState = State( + private val INCOMING_POP3_STATE = State( protocolType = IncomingProtocolType.POP3, - server = StringInputField(value = "pop3.domain.example"), + server = StringInputField(value = "pop3.example.org"), port = NumberInputField(value = 995), security = ConnectionSecurity.TLS, authenticationType = AuthenticationType.PasswordCleartext, username = StringInputField(value = "user"), password = StringInputField(value = "password"), clientCertificateAlias = null, - imapAutodetectNamespaceEnabled = true, - imapPrefix = StringInputField(value = "prefix"), - imapUseCompression = true, - imapSendClientId = true, ) - val result = incomingState.toServerSettings() - - assertThat(result).isEqualTo( - ServerSettings( - type = "pop3", - host = "pop3.domain.example", - port = 995, - connectionSecurity = MailConnectionSecurity.SSL_TLS_REQUIRED, - authenticationType = AuthType.PLAIN, - username = "user", - password = "password", - clientCertificateAlias = null, - ), + private val POP3_SERVER_SETTINGS = ServerSettings( + type = "pop3", + host = "pop3.example.org", + port = 995, + connectionSecurity = MailConnectionSecurity.SSL_TLS_REQUIRED, + authenticationType = AuthType.PLAIN, + username = "user", + password = "password", + clientCertificateAlias = null, ) } } diff --git a/feature/account/server/settings/src/test/kotlin/app/k9mail/feature/account/server/settings/ui/incoming/IncomingServerSettingsConfigStateTest.kt b/feature/account/server/settings/src/test/kotlin/app/k9mail/feature/account/server/settings/ui/incoming/IncomingServerSettingsStateTest.kt similarity index 96% rename from feature/account/server/settings/src/test/kotlin/app/k9mail/feature/account/server/settings/ui/incoming/IncomingServerSettingsConfigStateTest.kt rename to feature/account/server/settings/src/test/kotlin/app/k9mail/feature/account/server/settings/ui/incoming/IncomingServerSettingsStateTest.kt index a30aacf2618..4fdf8810b25 100644 --- a/feature/account/server/settings/src/test/kotlin/app/k9mail/feature/account/server/settings/ui/incoming/IncomingServerSettingsConfigStateTest.kt +++ b/feature/account/server/settings/src/test/kotlin/app/k9mail/feature/account/server/settings/ui/incoming/IncomingServerSettingsStateTest.kt @@ -11,7 +11,7 @@ import assertk.assertThat import assertk.assertions.isEqualTo import org.junit.Test -class IncomingServerSettingsConfigStateTest { +class IncomingServerSettingsStateTest { @Test fun `should set default values`() { diff --git a/feature/account/server/settings/src/test/kotlin/app/k9mail/feature/account/server/settings/ui/incoming/IncomingServerSettingsViewModelTest.kt b/feature/account/server/settings/src/test/kotlin/app/k9mail/feature/account/server/settings/ui/incoming/IncomingServerSettingsViewModelTest.kt index 8ca6ab6b072..d3f7c14f02a 100644 --- a/feature/account/server/settings/src/test/kotlin/app/k9mail/feature/account/server/settings/ui/incoming/IncomingServerSettingsViewModelTest.kt +++ b/feature/account/server/settings/src/test/kotlin/app/k9mail/feature/account/server/settings/ui/incoming/IncomingServerSettingsViewModelTest.kt @@ -26,6 +26,7 @@ import assertk.assertThat import assertk.assertions.isEqualTo import com.fsck.k9.mail.AuthType import com.fsck.k9.mail.ServerSettings +import com.fsck.k9.mail.store.imap.ImapStoreSettings import kotlinx.coroutines.test.runTest import org.junit.Rule import org.junit.Test @@ -48,7 +49,12 @@ class IncomingServerSettingsViewModelTest { "username", "password", clientCertificateAlias = null, - extra = emptyMap(), + extra = ImapStoreSettings.createExtra( + autoDetectNamespace = true, + pathPrefix = null, + useCompression = true, + sendClientId = true, + ), ), ) val repository = InMemoryAccountStateRepository( @@ -76,6 +82,10 @@ class IncomingServerSettingsViewModelTest { authenticationType = AuthenticationType.PasswordCleartext, username = StringInputField(value = "username"), password = StringInputField(value = "password"), + imapAutodetectNamespaceEnabled = true, + imapPrefix = StringInputField(value = ""), + imapUseCompression = true, + imapSendClientId = true, ), ) } diff --git a/feature/account/server/settings/src/test/kotlin/app/k9mail/feature/account/server/settings/ui/outgoing/OutgoingServerSettingsStateMapperKtTest.kt b/feature/account/server/settings/src/test/kotlin/app/k9mail/feature/account/server/settings/ui/outgoing/OutgoingServerSettingsStateMapperKtTest.kt index f657f33f4e4..84f5a9d9cc9 100644 --- a/feature/account/server/settings/src/test/kotlin/app/k9mail/feature/account/server/settings/ui/outgoing/OutgoingServerSettingsStateMapperKtTest.kt +++ b/feature/account/server/settings/src/test/kotlin/app/k9mail/feature/account/server/settings/ui/outgoing/OutgoingServerSettingsStateMapperKtTest.kt @@ -1,5 +1,6 @@ package app.k9mail.feature.account.server.settings.ui.outgoing +import app.k9mail.feature.account.common.domain.entity.AccountState import app.k9mail.feature.account.common.domain.entity.AuthenticationType import app.k9mail.feature.account.common.domain.entity.ConnectionSecurity import app.k9mail.feature.account.common.domain.entity.MailConnectionSecurity @@ -15,8 +16,39 @@ import org.junit.Test class OutgoingServerSettingsStateMapperKtTest { @Test - fun `should map to server settings`() { - val outgoingState = State( + fun `should map to state with email as username when server settings are null`() { + val accountState = AccountState( + emailAddress = "test@example.com", + outgoingServerSettings = null, + ) + + val result = accountState.toOutgoingServerSettingsState() + + assertThat(result).isEqualTo(State(username = StringInputField(value = "test@example.com"))) + } + + @Test + fun `should map from SMTP server settings to state`() { + val accountState = AccountState( + outgoingServerSettings = SMTP_SERVER_SETTINGS, + ) + + val result = accountState.toOutgoingServerSettingsState() + + assertThat(result).isEqualTo(OUTGOING_STATE) + } + + @Test + fun `should map state to server settings`() { + val outgoingState = OUTGOING_STATE + + val result = outgoingState.toServerSettings() + + assertThat(result).isEqualTo(SMTP_SERVER_SETTINGS) + } + + private companion object { + private val OUTGOING_STATE = State( server = StringInputField(value = "smtp.example.org"), port = NumberInputField(value = 587), security = ConnectionSecurity.TLS, @@ -26,19 +58,15 @@ class OutgoingServerSettingsStateMapperKtTest { clientCertificateAlias = null, ) - val result = outgoingState.toServerSettings() - - assertThat(result).isEqualTo( - ServerSettings( - type = "smtp", - host = "smtp.example.org", - port = 587, - connectionSecurity = MailConnectionSecurity.SSL_TLS_REQUIRED, - authenticationType = AuthType.PLAIN, - username = "user", - password = "password", - clientCertificateAlias = null, - ), + private val SMTP_SERVER_SETTINGS = ServerSettings( + type = "smtp", + host = "smtp.example.org", + port = 587, + connectionSecurity = MailConnectionSecurity.SSL_TLS_REQUIRED, + authenticationType = AuthType.PLAIN, + username = "user", + password = "password", + clientCertificateAlias = null, ) } } diff --git a/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/domain/AutoDiscoveryMapper.kt b/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/domain/AutoDiscoveryMapper.kt index 6291e02ddeb..7013dcdb04d 100644 --- a/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/domain/AutoDiscoveryMapper.kt +++ b/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/domain/AutoDiscoveryMapper.kt @@ -9,6 +9,7 @@ import app.k9mail.feature.account.common.domain.entity.toMailConnectionSecurity import app.k9mail.feature.account.setup.domain.entity.toAuthenticationType import app.k9mail.feature.account.setup.domain.entity.toConnectionSecurity import com.fsck.k9.mail.ServerSettings +import com.fsck.k9.mail.store.imap.ImapStoreSettings internal fun IncomingServerSettings.toServerSettings(password: String?): ServerSettings { return when (this) { @@ -27,7 +28,12 @@ private fun ImapServerSettings.toImapServerSettings(password: String?): ServerSe username = username, password = password, clientCertificateAlias = null, - extra = emptyMap(), + extra = ImapStoreSettings.createExtra( + autoDetectNamespace = true, + pathPrefix = null, + useCompression = true, + sendClientId = true, + ), ) } diff --git a/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/ui/autodiscovery/AccountAutoDiscoveryContent.kt b/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/ui/autodiscovery/AccountAutoDiscoveryContent.kt index 1b1a48b8832..0a91247c7e1 100644 --- a/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/ui/autodiscovery/AccountAutoDiscoveryContent.kt +++ b/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/ui/autodiscovery/AccountAutoDiscoveryContent.kt @@ -8,14 +8,12 @@ import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import app.k9mail.core.ui.compose.common.DevicePreviews -import app.k9mail.core.ui.compose.designsystem.molecule.ContentLoadingErrorState import app.k9mail.core.ui.compose.designsystem.molecule.ContentLoadingErrorView import app.k9mail.core.ui.compose.designsystem.molecule.ErrorView import app.k9mail.core.ui.compose.designsystem.molecule.LoadingView @@ -23,6 +21,7 @@ import app.k9mail.core.ui.compose.designsystem.template.ResponsiveWidthContainer import app.k9mail.core.ui.compose.theme.K9Theme import app.k9mail.core.ui.compose.theme.MainTheme import app.k9mail.core.ui.compose.theme.ThunderbirdTheme +import app.k9mail.feature.account.common.ui.loadingerror.rememberContentLoadingErrorViewState import app.k9mail.feature.account.oauth.ui.AccountOAuthContract import app.k9mail.feature.account.oauth.ui.preview.PreviewAccountOAuthViewModel import app.k9mail.feature.account.setup.R @@ -46,16 +45,9 @@ internal fun AccountAutoDiscoveryContent( .then(modifier), ) { val resources = LocalContext.current.resources - val viewState = remember(key1 = state.isLoading, key2 = state.error) { - when { - state.isLoading -> ContentLoadingErrorState.Loading - state.error != null -> ContentLoadingErrorState.Error - else -> ContentLoadingErrorState.Content - } - } ContentLoadingErrorView( - state = viewState, + state = rememberContentLoadingErrorViewState(state), loading = { LoadingView( message = stringResource(id = R.string.account_setup_auto_discovery_loading_message), diff --git a/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/ui/autodiscovery/AccountAutoDiscoveryContract.kt b/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/ui/autodiscovery/AccountAutoDiscoveryContract.kt index 287bfdae1c6..d54b4ac1215 100644 --- a/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/ui/autodiscovery/AccountAutoDiscoveryContract.kt +++ b/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/ui/autodiscovery/AccountAutoDiscoveryContract.kt @@ -6,6 +6,7 @@ import app.k9mail.core.ui.compose.common.mvi.UnidirectionalViewModel import app.k9mail.feature.account.common.domain.entity.AuthorizationState import app.k9mail.feature.account.common.domain.input.BooleanInputField import app.k9mail.feature.account.common.domain.input.StringInputField +import app.k9mail.feature.account.common.ui.loadingerror.LoadingErrorState import app.k9mail.feature.account.oauth.domain.entity.OAuthResult import app.k9mail.feature.account.oauth.ui.AccountOAuthContract @@ -32,11 +33,11 @@ interface AccountAutoDiscoveryContract { val authorizationState: AuthorizationState? = null, val isSuccess: Boolean = false, - val error: Error? = null, - val isLoading: Boolean = false, + override val error: Error? = null, + override val isLoading: Boolean = false, val isNextButtonVisible: Boolean = true, - ) + ) : LoadingErrorState sealed interface Event { data class EmailAddressChanged(val emailAddress: String) : Event @@ -44,16 +45,16 @@ interface AccountAutoDiscoveryContract { data class ResultApprovalChanged(val confirmed: Boolean) : Event data class OnOAuthResult(val result: OAuthResult) : Event - object OnNextClicked : Event - object OnBackClicked : Event - object OnRetryClicked : Event - object OnEditConfigurationClicked : Event + data object OnNextClicked : Event + data object OnBackClicked : Event + data object OnRetryClicked : Event + data object OnEditConfigurationClicked : Event } sealed class Effect { data class NavigateNext(val isAutomaticConfig: Boolean) : Effect() - object NavigateBack : Effect() + data object NavigateBack : Effect() } interface Validator { @@ -63,7 +64,7 @@ interface AccountAutoDiscoveryContract { } sealed interface Error { - object NetworkError : Error - object UnknownError : Error + data object NetworkError : Error + data object UnknownError : Error } } diff --git a/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/ui/autodiscovery/AccountAutoDiscoveryStateMapper.kt b/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/ui/autodiscovery/AccountAutoDiscoveryStateMapper.kt index 7aa4f6846ed..3cf564de9e4 100644 --- a/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/ui/autodiscovery/AccountAutoDiscoveryStateMapper.kt +++ b/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/ui/autodiscovery/AccountAutoDiscoveryStateMapper.kt @@ -39,6 +39,10 @@ internal fun AccountAutoDiscoveryContract.State.toIncomingConfigState(): Incomin authenticationType = incomingSettings.authenticationTypes.first().toAuthenticationType(), username = StringInputField(value = incomingSettings.username), password = StringInputField(value = password.value), + imapAutodetectNamespaceEnabled = true, + imapPrefix = StringInputField(value = ""), + imapUseCompression = true, + imapSendClientId = true, ) } } diff --git a/feature/account/setup/src/test/kotlin/app/k9mail/feature/account/setup/domain/AutoDiscoveryMapperKtTest.kt b/feature/account/setup/src/test/kotlin/app/k9mail/feature/account/setup/domain/AutoDiscoveryMapperKtTest.kt index d254912ae2b..c5a9b2fe8a0 100644 --- a/feature/account/setup/src/test/kotlin/app/k9mail/feature/account/setup/domain/AutoDiscoveryMapperKtTest.kt +++ b/feature/account/setup/src/test/kotlin/app/k9mail/feature/account/setup/domain/AutoDiscoveryMapperKtTest.kt @@ -13,6 +13,7 @@ import assertk.assertThat import assertk.assertions.isEqualTo import com.fsck.k9.mail.AuthType import com.fsck.k9.mail.ServerSettings +import com.fsck.k9.mail.store.imap.ImapStoreSettings import kotlin.test.assertFailsWith import org.junit.Test @@ -41,7 +42,12 @@ class AutoDiscoveryMapperKtTest { username = "user", password = "password", clientCertificateAlias = null, - extra = emptyMap(), + extra = ImapStoreSettings.createExtra( + autoDetectNamespace = true, + pathPrefix = null, + useCompression = true, + sendClientId = true, + ), ), ) } diff --git a/feature/launcher/src/main/kotlin/app/k9mail/feature/launcher/FeatureLauncherActivity.kt b/feature/launcher/src/main/kotlin/app/k9mail/feature/launcher/FeatureLauncherActivity.kt index 3af2504f2d9..e43d70f24b0 100644 --- a/feature/launcher/src/main/kotlin/app/k9mail/feature/launcher/FeatureLauncherActivity.kt +++ b/feature/launcher/src/main/kotlin/app/k9mail/feature/launcher/FeatureLauncherActivity.kt @@ -7,8 +7,8 @@ import androidx.activity.ComponentActivity import androidx.core.view.WindowCompat import app.k9mail.core.ui.compose.common.activity.setActivityContent import app.k9mail.core.ui.compose.common.navigation.toDeepLinkUri -import app.k9mail.feature.account.edit.navigation.NAVIGATION_ROUTE_ACCOUNT_EDIT_CONFIG_INCOMING -import app.k9mail.feature.account.edit.navigation.NAVIGATION_ROUTE_ACCOUNT_EDIT_CONFIG_OUTGOING +import app.k9mail.feature.account.edit.navigation.NAVIGATION_ROUTE_ACCOUNT_EDIT_SERVER_SETTINGS_INCOMING +import app.k9mail.feature.account.edit.navigation.NAVIGATION_ROUTE_ACCOUNT_EDIT_SERVER_SETTINGS_OUTGOING import app.k9mail.feature.account.edit.navigation.withAccountUuid import app.k9mail.feature.account.setup.navigation.NAVIGATION_ROUTE_ACCOUNT_SETUP import app.k9mail.feature.launcher.ui.FeatureLauncherApp @@ -47,7 +47,8 @@ class FeatureLauncherActivity : ComponentActivity() { @JvmStatic fun launchEditIncomingSettings(context: Context, accountUuid: String) { val intent = Intent(context, FeatureLauncherActivity::class.java).apply { - data = NAVIGATION_ROUTE_ACCOUNT_EDIT_CONFIG_INCOMING.withAccountUuid(accountUuid).toDeepLinkUri() + data = NAVIGATION_ROUTE_ACCOUNT_EDIT_SERVER_SETTINGS_INCOMING + .withAccountUuid(accountUuid).toDeepLinkUri() } context.startActivity(intent) } @@ -55,7 +56,8 @@ class FeatureLauncherActivity : ComponentActivity() { @JvmStatic fun launchEditOutgoingSettings(context: Context, accountUuid: String) { val intent = Intent(context, FeatureLauncherActivity::class.java).apply { - data = NAVIGATION_ROUTE_ACCOUNT_EDIT_CONFIG_OUTGOING.withAccountUuid(accountUuid).toDeepLinkUri() + data = NAVIGATION_ROUTE_ACCOUNT_EDIT_SERVER_SETTINGS_OUTGOING + .withAccountUuid(accountUuid).toDeepLinkUri() } context.startActivity(intent) }