diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml
index 8d7bd0145..0858e3ff9 100644
--- a/.idea/codeStyles/Project.xml
+++ b/.idea/codeStyles/Project.xml
@@ -20,11 +20,6 @@
-
-
diff --git a/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/AccountSetupModule.kt b/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/AccountSetupModule.kt
index e92f1d53c..1838dc06e 100644
--- a/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/AccountSetupModule.kt
+++ b/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/AccountSetupModule.kt
@@ -1,6 +1,7 @@
package app.k9mail.feature.account.setup
import app.k9mail.feature.account.setup.ui.AccountSetupViewModel
+import app.k9mail.feature.account.setup.ui.incoming.AccountIncomingConfigViewModel
import app.k9mail.feature.account.setup.ui.options.AccountOptionsContract
import app.k9mail.feature.account.setup.ui.options.AccountOptionsValidator
import app.k9mail.feature.account.setup.ui.options.AccountOptionsViewModel
@@ -16,6 +17,7 @@ val featureAccountSetupModule: Module = module {
factory { AccountOptionsValidator() }
viewModel { AccountSetupViewModel() }
+ viewModel { AccountIncomingConfigViewModel() }
viewModel {
AccountOutgoingConfigViewModel(
validator = get(),
diff --git a/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/domain/entity/ConnectionSecurity.kt b/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/domain/entity/ConnectionSecurity.kt
index 5722ed843..fae553a85 100644
--- a/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/domain/entity/ConnectionSecurity.kt
+++ b/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/domain/entity/ConnectionSecurity.kt
@@ -34,3 +34,12 @@ fun ConnectionSecurity.toImapDefaultPort(): Long {
TLS -> 993
}
}
+
+@Suppress("MagicNumber")
+fun ConnectionSecurity.toPop3DefaultPort(): Long {
+ return when (this) {
+ None -> 110
+ StartTLS -> 110
+ TLS -> 995
+ }
+}
diff --git a/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/domain/entity/IncomingProtocolType.kt b/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/domain/entity/IncomingProtocolType.kt
new file mode 100644
index 000000000..63fe318a6
--- /dev/null
+++ b/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/domain/entity/IncomingProtocolType.kt
@@ -0,0 +1,30 @@
+package app.k9mail.feature.account.setup.domain.entity
+
+import kotlinx.collections.immutable.toImmutableList
+
+enum class IncomingProtocolType {
+ IMAP,
+ POP3,
+ ;
+
+ companion object {
+ val DEFAULT = IMAP
+
+ fun all() = values().toList().toImmutableList()
+ }
+}
+
+@Suppress("SameReturnValue")
+fun IncomingProtocolType.toDefaultSecurity(): ConnectionSecurity {
+ return when (this) {
+ IncomingProtocolType.IMAP -> ConnectionSecurity.TLS
+ IncomingProtocolType.POP3 -> ConnectionSecurity.TLS
+ }
+}
+
+fun IncomingProtocolType.toDefaultPort(connectionSecurity: ConnectionSecurity): Long {
+ return when (this) {
+ IncomingProtocolType.IMAP -> connectionSecurity.toImapDefaultPort()
+ IncomingProtocolType.POP3 -> connectionSecurity.toPop3DefaultPort()
+ }
+}
diff --git a/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/domain/input/NumberInputField.kt b/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/domain/input/NumberInputField.kt
index d6f768f0b..16fd3b468 100644
--- a/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/domain/input/NumberInputField.kt
+++ b/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/domain/input/NumberInputField.kt
@@ -3,7 +3,7 @@ package app.k9mail.feature.account.setup.domain.input
import app.k9mail.core.common.domain.usecase.validation.ValidationError
import app.k9mail.core.common.domain.usecase.validation.ValidationResult
-data class NumberInputField(
+class NumberInputField(
override val value: Long? = null,
override val error: ValidationError? = null,
override val isValid: Boolean = false,
@@ -37,15 +37,35 @@ data class NumberInputField(
override fun updateFromValidationResult(result: ValidationResult): NumberInputField {
return when (result) {
- is ValidationResult.Success -> copy(
+ is ValidationResult.Success -> NumberInputField(
+ value = value,
error = null,
isValid = true,
)
- is ValidationResult.Failure -> copy(
+ is ValidationResult.Failure -> NumberInputField(
+ value = value,
error = result.error,
isValid = false,
)
}
}
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as NumberInputField
+
+ if (value != other.value) return false
+ if (error != other.error) return false
+ return isValid == other.isValid
+ }
+
+ override fun hashCode(): Int {
+ var result = value?.hashCode() ?: 0
+ result = 31 * result + (error?.hashCode() ?: 0)
+ result = 31 * result + isValid.hashCode()
+ return result
+ }
}
diff --git a/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/domain/input/StringInputField.kt b/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/domain/input/StringInputField.kt
index 770bf80e2..466863c0e 100644
--- a/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/domain/input/StringInputField.kt
+++ b/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/domain/input/StringInputField.kt
@@ -3,7 +3,7 @@ package app.k9mail.feature.account.setup.domain.input
import app.k9mail.core.common.domain.usecase.validation.ValidationError
import app.k9mail.core.common.domain.usecase.validation.ValidationResult
-data class StringInputField(
+class StringInputField(
override val value: String = "",
override val error: ValidationError? = null,
override val isValid: Boolean = false,
@@ -37,15 +37,35 @@ data class StringInputField(
override fun updateFromValidationResult(result: ValidationResult): StringInputField {
return when (result) {
- is ValidationResult.Success -> copy(
+ is ValidationResult.Success -> StringInputField(
+ value = value,
error = null,
isValid = true,
)
- is ValidationResult.Failure -> copy(
+ is ValidationResult.Failure -> StringInputField(
+ value = value,
error = result.error,
isValid = false,
)
}
}
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as StringInputField
+
+ if (value != other.value) return false
+ if (error != other.error) return false
+ return isValid == other.isValid
+ }
+
+ override fun hashCode(): Int {
+ var result = value.hashCode()
+ result = 31 * result + (error?.hashCode() ?: 0)
+ result = 31 * result + isValid.hashCode()
+ return result
+ }
}
diff --git a/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/ui/AccountSetupScreen.kt b/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/ui/AccountSetupScreen.kt
index 160b3aaff..ba5a67922 100644
--- a/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/ui/AccountSetupScreen.kt
+++ b/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/ui/AccountSetupScreen.kt
@@ -8,7 +8,9 @@ import app.k9mail.feature.account.setup.ui.AccountSetupContract.Event
import app.k9mail.feature.account.setup.ui.AccountSetupContract.SetupStep
import app.k9mail.feature.account.setup.ui.AccountSetupContract.ViewModel
import app.k9mail.feature.account.setup.ui.autoconfig.AccountAutoConfigScreen
+import app.k9mail.feature.account.setup.ui.incoming.AccountIncomingConfigContract
import app.k9mail.feature.account.setup.ui.incoming.AccountIncomingConfigScreen
+import app.k9mail.feature.account.setup.ui.incoming.AccountIncomingConfigViewModel
import app.k9mail.feature.account.setup.ui.options.AccountOptionsContract
import app.k9mail.feature.account.setup.ui.options.AccountOptionsScreen
import app.k9mail.feature.account.setup.ui.options.AccountOptionsViewModel
@@ -22,6 +24,7 @@ fun AccountSetupScreen(
onFinish: () -> Unit,
onBack: () -> Unit,
viewModel: ViewModel = koinViewModel(),
+ incomingViewModel: AccountIncomingConfigContract.ViewModel = koinViewModel(),
outgoingViewModel: AccountOutgoingConfigContract.ViewModel = koinViewModel(),
optionsViewModel: AccountOptionsContract.ViewModel = koinViewModel(),
) {
@@ -44,6 +47,7 @@ fun AccountSetupScreen(
AccountIncomingConfigScreen(
onNext = { dispatch(Event.OnNext) },
onBack = { dispatch(Event.OnBack) },
+ viewModel = incomingViewModel,
)
}
diff --git a/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/ui/AccountSetupStringMapper.kt b/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/ui/AccountSetupStringMapper.kt
new file mode 100644
index 000000000..88e286873
--- /dev/null
+++ b/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/ui/AccountSetupStringMapper.kt
@@ -0,0 +1,13 @@
+package app.k9mail.feature.account.setup.ui
+
+import android.content.res.Resources
+import app.k9mail.feature.account.setup.R
+import app.k9mail.feature.account.setup.domain.entity.ConnectionSecurity
+
+internal fun ConnectionSecurity.toResourceString(resources: Resources): String {
+ return when (this) {
+ ConnectionSecurity.None -> resources.getString(R.string.account_setup_connection_security_none)
+ ConnectionSecurity.StartTLS -> resources.getString(R.string.account_setup_connection_security_start_tls)
+ ConnectionSecurity.TLS -> resources.getString(R.string.account_setup_connection_security_ssl)
+ }
+}
diff --git a/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/ui/incoming/AccountIncomingConfigContent.kt b/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/ui/incoming/AccountIncomingConfigContent.kt
index c961dfc7e..8983b08a7 100644
--- a/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/ui/incoming/AccountIncomingConfigContent.kt
+++ b/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/ui/incoming/AccountIncomingConfigContent.kt
@@ -2,27 +2,48 @@ package app.k9mail.feature.account.setup.ui.incoming
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.requiredHeight
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.runtime.Composable
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.atom.text.TextBody1
+import app.k9mail.core.ui.compose.designsystem.molecule.input.CheckboxInput
+import app.k9mail.core.ui.compose.designsystem.molecule.input.NumberInput
+import app.k9mail.core.ui.compose.designsystem.molecule.input.PasswordInput
+import app.k9mail.core.ui.compose.designsystem.molecule.input.SelectInput
+import app.k9mail.core.ui.compose.designsystem.molecule.input.TextInput
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.setup.R
+import app.k9mail.feature.account.setup.domain.entity.ConnectionSecurity
+import app.k9mail.feature.account.setup.domain.entity.IncomingProtocolType
+import app.k9mail.feature.account.setup.ui.common.defaultItemPadding
+import app.k9mail.feature.account.setup.ui.incoming.AccountIncomingConfigContract.Event
+import app.k9mail.feature.account.setup.ui.incoming.AccountIncomingConfigContract.State
+import app.k9mail.feature.account.setup.ui.toResourceString
+import kotlinx.collections.immutable.persistentListOf
+@Suppress("LongMethod")
@Composable
internal fun AccountIncomingConfigContent(
+ state: State,
+ onEvent: (Event) -> Unit,
contentPadding: PaddingValues,
modifier: Modifier = Modifier,
) {
+ val resources = LocalContext.current.resources
+
ResponsiveWidthContainer(
modifier = Modifier
.testTag("AccountIncomingConfigContent")
@@ -38,7 +59,110 @@ internal fun AccountIncomingConfigContent(
verticalArrangement = Arrangement.spacedBy(MainTheme.spacings.default),
) {
item {
- TextBody1(text = "Incoming Config")
+ Spacer(modifier = Modifier.requiredHeight(MainTheme.sizes.smaller))
+ }
+
+ item {
+ SelectInput(
+ options = IncomingProtocolType.all(),
+ selectedOption = state.protocolType,
+ onOptionChange = { onEvent(Event.ProtocolTypeChanged(it)) },
+ label = stringResource(id = R.string.account_setup_incoming_config_protocol_type_label),
+ contentPadding = defaultItemPadding(),
+ )
+ }
+
+ item {
+ TextInput(
+ text = state.server.value,
+ onTextChange = { onEvent(Event.ServerChanged(it)) },
+ label = stringResource(id = R.string.account_setup_incoming_config_server_label),
+ contentPadding = defaultItemPadding(),
+ )
+ }
+
+ item {
+ SelectInput(
+ options = ConnectionSecurity.all(),
+ optionToStringTransformation = { it.toResourceString(resources) },
+ selectedOption = state.security,
+ onOptionChange = { onEvent(Event.SecurityChanged(it)) },
+ label = stringResource(id = R.string.account_setup_incoming_config_security_label),
+ contentPadding = defaultItemPadding(),
+ )
+ }
+
+ item {
+ NumberInput(
+ value = state.port.value,
+ onValueChange = { onEvent(Event.PortChanged(it)) },
+ label = stringResource(id = R.string.account_setup_outgoing_config_port_label),
+ contentPadding = defaultItemPadding(),
+ )
+ }
+
+ item {
+ TextInput(
+ text = state.username.value,
+ onTextChange = { onEvent(Event.UsernameChanged(it)) },
+ label = stringResource(id = R.string.account_setup_outgoing_config_username_label),
+ contentPadding = defaultItemPadding(),
+ )
+ }
+
+ item {
+ PasswordInput(
+ password = state.password.value,
+ onPasswordChange = { onEvent(Event.PasswordChanged(it)) },
+ contentPadding = defaultItemPadding(),
+ )
+ }
+
+ item {
+ // TODO add client certificate support
+ SelectInput(
+ options = persistentListOf(
+ stringResource(
+ id = R.string.account_setup_client_certificate_none_available,
+ ),
+ ),
+ optionToStringTransformation = { it },
+ selectedOption = stringResource(
+ id = R.string.account_setup_client_certificate_none_available,
+ ),
+ onOptionChange = { onEvent(Event.ClientCertificateChanged(it)) },
+ label = stringResource(id = R.string.account_setup_outgoing_config_client_certificate_label),
+ contentPadding = defaultItemPadding(),
+ )
+ }
+
+ if (state.protocolType == IncomingProtocolType.IMAP) {
+ item {
+ CheckboxInput(
+ text = stringResource(id = R.string.account_setup_incoming_config_imap_namespace_label),
+ checked = state.imapAutodetectNamespaceEnabled,
+ onCheckedChange = { onEvent(Event.ImapAutoDetectNamespaceChanged(it)) },
+ contentPadding = defaultItemPadding(),
+ )
+ }
+
+ item {
+ TextInput(
+ text = state.imapPrefix.value,
+ onTextChange = { onEvent(Event.ImapPrefixChanged(it)) },
+ label = stringResource(id = R.string.account_setup_incoming_config_imap_prefix_label),
+ contentPadding = defaultItemPadding(),
+ )
+ }
+ }
+
+ item {
+ CheckboxInput(
+ text = stringResource(id = R.string.account_setup_incoming_config_compression_label),
+ checked = state.useCompression,
+ onCheckedChange = { onEvent(Event.UseCompressionChanged(it)) },
+ contentPadding = defaultItemPadding(),
+ )
}
}
}
@@ -49,6 +173,8 @@ internal fun AccountIncomingConfigContent(
internal fun AccountIncomingConfigContentK9Preview() {
K9Theme {
AccountIncomingConfigContent(
+ onEvent = { },
+ state = State(),
contentPadding = PaddingValues(),
)
}
@@ -59,6 +185,8 @@ internal fun AccountIncomingConfigContentK9Preview() {
internal fun AccountIncomingConfigContentThunderbirdPreview() {
ThunderbirdTheme {
AccountIncomingConfigContent(
+ onEvent = { },
+ state = State(),
contentPadding = PaddingValues(),
)
}
diff --git a/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/ui/incoming/AccountIncomingConfigContract.kt b/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/ui/incoming/AccountIncomingConfigContract.kt
new file mode 100644
index 000000000..51441f7d5
--- /dev/null
+++ b/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/ui/incoming/AccountIncomingConfigContract.kt
@@ -0,0 +1,51 @@
+package app.k9mail.feature.account.setup.ui.incoming
+
+import app.k9mail.core.ui.compose.common.mvi.UnidirectionalViewModel
+import app.k9mail.feature.account.setup.domain.entity.ConnectionSecurity
+import app.k9mail.feature.account.setup.domain.entity.IncomingProtocolType
+import app.k9mail.feature.account.setup.domain.entity.toDefaultPort
+import app.k9mail.feature.account.setup.domain.entity.toDefaultSecurity
+import app.k9mail.feature.account.setup.domain.input.NumberInputField
+import app.k9mail.feature.account.setup.domain.input.StringInputField
+
+interface AccountIncomingConfigContract {
+
+ interface ViewModel : UnidirectionalViewModel {
+ fun initState(state: State)
+ }
+
+ data class State(
+ val protocolType: IncomingProtocolType = IncomingProtocolType.DEFAULT,
+ val server: StringInputField = StringInputField(),
+ val security: ConnectionSecurity = IncomingProtocolType.DEFAULT.toDefaultSecurity(),
+ val port: NumberInputField = NumberInputField(
+ IncomingProtocolType.DEFAULT.toDefaultPort(IncomingProtocolType.DEFAULT.toDefaultSecurity()),
+ ),
+ val username: StringInputField = StringInputField(),
+ val password: StringInputField = StringInputField(),
+ val clientCertificate: String = "",
+ val imapAutodetectNamespaceEnabled: Boolean = true,
+ val imapPrefix: StringInputField = StringInputField(),
+ val useCompression: Boolean = true,
+ )
+
+ sealed class Event {
+ data class ProtocolTypeChanged(val protocolType: IncomingProtocolType) : Event()
+ data class ServerChanged(val server: String) : Event()
+ data class SecurityChanged(val security: ConnectionSecurity) : Event()
+ data class PortChanged(val port: Long?) : Event()
+ data class UsernameChanged(val username: String) : Event()
+ data class PasswordChanged(val password: String) : Event()
+ data class ClientCertificateChanged(val clientCertificate: String) : Event()
+ data class ImapAutoDetectNamespaceChanged(val enabled: Boolean) : Event()
+ data class ImapPrefixChanged(val imapPrefix: String) : Event()
+ data class UseCompressionChanged(val useCompression: Boolean) : Event()
+ object OnNextClicked : Event()
+ object OnBackClicked : Event()
+ }
+
+ sealed class Effect {
+ object NavigateNext : Effect()
+ object NavigateBack : Effect()
+ }
+}
diff --git a/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/ui/incoming/AccountIncomingConfigScreen.kt b/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/ui/incoming/AccountIncomingConfigScreen.kt
index 5a47d9549..de606b0d2 100644
--- a/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/ui/incoming/AccountIncomingConfigScreen.kt
+++ b/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/ui/incoming/AccountIncomingConfigScreen.kt
@@ -4,19 +4,31 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import app.k9mail.core.ui.compose.common.DevicePreviews
+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.setup.R
import app.k9mail.feature.account.setup.ui.common.AccountSetupBottomBar
import app.k9mail.feature.account.setup.ui.common.AccountSetupTopAppBar
+import app.k9mail.feature.account.setup.ui.incoming.AccountIncomingConfigContract.Effect
+import app.k9mail.feature.account.setup.ui.incoming.AccountIncomingConfigContract.Event
+import app.k9mail.feature.account.setup.ui.incoming.AccountIncomingConfigContract.ViewModel
@Composable
fun AccountIncomingConfigScreen(
onNext: () -> Unit,
onBack: () -> Unit,
+ viewModel: ViewModel,
modifier: Modifier = Modifier,
) {
+ val (state, dispatch) = viewModel.observe { effect ->
+ when (effect) {
+ is Effect.NavigateNext -> onNext()
+ is Effect.NavigateBack -> onBack()
+ }
+ }
+
Scaffold(
topBar = {
AccountSetupTopAppBar(
@@ -27,13 +39,15 @@ fun AccountIncomingConfigScreen(
AccountSetupBottomBar(
nextButtonText = stringResource(id = R.string.account_setup_button_next),
backButtonText = stringResource(id = R.string.account_setup_button_back),
- onNextClick = onNext,
- onBackClick = onBack,
+ onNextClick = { dispatch(Event.OnNextClicked) },
+ onBackClick = { dispatch(Event.OnBackClicked) },
)
},
modifier = modifier,
) { innerPadding ->
AccountIncomingConfigContent(
+ onEvent = { dispatch(it) },
+ state = state.value,
contentPadding = innerPadding,
)
}
@@ -46,6 +60,7 @@ internal fun AccountIncomingConfigScreenK9Preview() {
AccountIncomingConfigScreen(
onNext = {},
onBack = {},
+ viewModel = AccountIncomingConfigViewModel(),
)
}
}
@@ -57,6 +72,7 @@ internal fun AccountIncomingConfigScreenThunderbirdPreview() {
AccountIncomingConfigScreen(
onNext = {},
onBack = {},
+ viewModel = AccountIncomingConfigViewModel(),
)
}
}
diff --git a/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/ui/incoming/AccountIncomingConfigViewModel.kt b/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/ui/incoming/AccountIncomingConfigViewModel.kt
new file mode 100644
index 000000000..f1b8a8174
--- /dev/null
+++ b/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/ui/incoming/AccountIncomingConfigViewModel.kt
@@ -0,0 +1,80 @@
+package app.k9mail.feature.account.setup.ui.incoming
+
+import app.k9mail.core.ui.compose.common.mvi.BaseViewModel
+import app.k9mail.feature.account.setup.domain.entity.ConnectionSecurity
+import app.k9mail.feature.account.setup.domain.entity.IncomingProtocolType
+import app.k9mail.feature.account.setup.domain.entity.toDefaultPort
+import app.k9mail.feature.account.setup.domain.entity.toDefaultSecurity
+import app.k9mail.feature.account.setup.ui.incoming.AccountIncomingConfigContract.Effect
+import app.k9mail.feature.account.setup.ui.incoming.AccountIncomingConfigContract.Event
+import app.k9mail.feature.account.setup.ui.incoming.AccountIncomingConfigContract.Event.ClientCertificateChanged
+import app.k9mail.feature.account.setup.ui.incoming.AccountIncomingConfigContract.Event.ImapAutoDetectNamespaceChanged
+import app.k9mail.feature.account.setup.ui.incoming.AccountIncomingConfigContract.Event.ImapPrefixChanged
+import app.k9mail.feature.account.setup.ui.incoming.AccountIncomingConfigContract.Event.OnBackClicked
+import app.k9mail.feature.account.setup.ui.incoming.AccountIncomingConfigContract.Event.OnNextClicked
+import app.k9mail.feature.account.setup.ui.incoming.AccountIncomingConfigContract.Event.PasswordChanged
+import app.k9mail.feature.account.setup.ui.incoming.AccountIncomingConfigContract.Event.PortChanged
+import app.k9mail.feature.account.setup.ui.incoming.AccountIncomingConfigContract.Event.ProtocolTypeChanged
+import app.k9mail.feature.account.setup.ui.incoming.AccountIncomingConfigContract.Event.SecurityChanged
+import app.k9mail.feature.account.setup.ui.incoming.AccountIncomingConfigContract.Event.ServerChanged
+import app.k9mail.feature.account.setup.ui.incoming.AccountIncomingConfigContract.Event.UseCompressionChanged
+import app.k9mail.feature.account.setup.ui.incoming.AccountIncomingConfigContract.Event.UsernameChanged
+import app.k9mail.feature.account.setup.ui.incoming.AccountIncomingConfigContract.State
+import app.k9mail.feature.account.setup.ui.incoming.AccountIncomingConfigContract.ViewModel
+
+class AccountIncomingConfigViewModel(
+ initialState: State = State(),
+) : BaseViewModel(initialState), ViewModel {
+
+ override fun initState(state: State) {
+ updateState {
+ state.copy()
+ }
+ }
+
+ override fun event(event: Event) {
+ when (event) {
+ is ProtocolTypeChanged -> updateProtocolType(event.protocolType)
+ is ServerChanged -> updateState { it.copy(server = it.server.updateValue(event.server)) }
+ is SecurityChanged -> updateSecurity(event.security)
+ is PortChanged -> updateState { it.copy(port = it.port.updateValue(event.port)) }
+ is UsernameChanged -> updateState { it.copy(username = it.username.updateValue(event.username)) }
+ is PasswordChanged -> updateState { it.copy(password = it.password.updateValue(event.password)) }
+ is ClientCertificateChanged -> updateState { it.copy(clientCertificate = event.clientCertificate) }
+ is ImapAutoDetectNamespaceChanged -> updateState { it.copy(imapAutodetectNamespaceEnabled = event.enabled) }
+ is ImapPrefixChanged -> updateState { it.copy(imapPrefix = it.imapPrefix.updateValue(event.imapPrefix)) }
+ is UseCompressionChanged -> updateState { it.copy(useCompression = event.useCompression) }
+ OnBackClicked -> navigateBack()
+ OnNextClicked -> submit()
+ }
+ }
+
+ private fun updateProtocolType(protocolType: IncomingProtocolType) {
+ updateState {
+ it.copy(
+ protocolType = protocolType,
+ security = protocolType.toDefaultSecurity(),
+ port = it.port.updateValue(
+ protocolType.toDefaultPort(protocolType.toDefaultSecurity()),
+ ),
+ )
+ }
+ }
+
+ private fun updateSecurity(security: ConnectionSecurity) {
+ updateState {
+ it.copy(
+ security = security,
+ port = it.port.updateValue(it.protocolType.toDefaultPort(security)),
+ )
+ }
+ }
+
+ private fun submit() {
+ navigateNext()
+ }
+
+ private fun navigateBack() = emitEffect(Effect.NavigateBack)
+
+ private fun navigateNext() = emitEffect(Effect.NavigateNext)
+}
diff --git a/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/ui/outgoing/AccountOutgoingConfigContent.kt b/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/ui/outgoing/AccountOutgoingConfigContent.kt
index 90047b622..5bc45ca58 100644
--- a/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/ui/outgoing/AccountOutgoingConfigContent.kt
+++ b/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/ui/outgoing/AccountOutgoingConfigContent.kt
@@ -30,6 +30,7 @@ import app.k9mail.feature.account.setup.domain.entity.ConnectionSecurity
import app.k9mail.feature.account.setup.ui.common.defaultItemPadding
import app.k9mail.feature.account.setup.ui.outgoing.AccountOutgoingConfigContract.Event
import app.k9mail.feature.account.setup.ui.outgoing.AccountOutgoingConfigContract.State
+import app.k9mail.feature.account.setup.ui.toResourceString
import kotlinx.collections.immutable.persistentListOf
@Suppress("LongMethod")
@@ -119,12 +120,12 @@ internal fun AccountOutgoingConfigContent(
SelectInput(
options = persistentListOf(
stringResource(
- id = R.string.account_setup_outgoing_config_client_certificate_none_available,
+ id = R.string.account_setup_client_certificate_none_available,
),
),
optionToStringTransformation = { it },
selectedOption = stringResource(
- id = R.string.account_setup_outgoing_config_client_certificate_none_available,
+ id = R.string.account_setup_client_certificate_none_available,
),
onOptionChange = { onEvent(Event.ClientCertificateChanged(it)) },
label = stringResource(id = R.string.account_setup_outgoing_config_client_certificate_label),
diff --git a/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/ui/outgoing/AccountOutgoingConfigViewModel.kt b/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/ui/outgoing/AccountOutgoingConfigViewModel.kt
index e4ffe39f0..7b57096d8 100644
--- a/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/ui/outgoing/AccountOutgoingConfigViewModel.kt
+++ b/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/ui/outgoing/AccountOutgoingConfigViewModel.kt
@@ -33,17 +33,17 @@ class AccountOutgoingConfigViewModel(
override fun event(event: Event) {
when (event) {
- is ServerChanged -> updateState { it.copy(server = it.server.copy(value = event.server)) }
+ is ServerChanged -> updateState { it.copy(server = it.server.updateValue(event.server)) }
is SecurityChanged -> updateSecurity(event.security)
- is PortChanged -> updateState { it.copy(port = it.port.copy(value = event.port)) }
- is UsernameChanged -> updateState { it.copy(username = it.username.copy(value = event.username)) }
- is PasswordChanged -> updateState { it.copy(password = it.password.copy(value = event.password)) }
+ is PortChanged -> updateState { it.copy(port = it.port.updateValue(event.port)) }
+ is UsernameChanged -> updateState { it.copy(username = it.username.updateValue(event.username)) }
+ is PasswordChanged -> updateState { it.copy(password = it.password.updateValue(event.password)) }
is ClientCertificateChanged -> updateState { it.copy(clientCertificate = event.clientCertificate) }
is ImapAutoDetectNamespaceChanged -> updateState { it.copy(imapAutodetectNamespaceEnabled = event.enabled) }
is UseCompressionChanged -> updateState { it.copy(useCompression = event.useCompression) }
- OnBackClicked -> navigateBack()
OnNextClicked -> submit()
+ OnBackClicked -> navigateBack()
}
}
@@ -51,10 +51,7 @@ class AccountOutgoingConfigViewModel(
updateState {
it.copy(
security = security,
- port = it.port.copy(
- value = security.toSmtpDefaultPort(),
- error = null,
- ),
+ port = it.port.updateValue(security.toSmtpDefaultPort()),
)
}
}
diff --git a/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/ui/outgoing/AccountOutgoingStringMapper.kt b/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/ui/outgoing/AccountOutgoingStringMapper.kt
index 258bfd68b..a72bd0763 100644
--- a/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/ui/outgoing/AccountOutgoingStringMapper.kt
+++ b/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/ui/outgoing/AccountOutgoingStringMapper.kt
@@ -3,20 +3,11 @@ package app.k9mail.feature.account.setup.ui.outgoing
import android.content.res.Resources
import app.k9mail.core.common.domain.usecase.validation.ValidationError
import app.k9mail.feature.account.setup.R
-import app.k9mail.feature.account.setup.domain.entity.ConnectionSecurity
import app.k9mail.feature.account.setup.domain.usecase.ValidatePassword.ValidatePasswordError
import app.k9mail.feature.account.setup.domain.usecase.ValidatePort.ValidatePortError
import app.k9mail.feature.account.setup.domain.usecase.ValidateServer.ValidateServerError
import app.k9mail.feature.account.setup.domain.usecase.ValidateUsername.ValidateUsernameError
-internal fun ConnectionSecurity.toResourceString(resources: Resources): String {
- return when (this) {
- ConnectionSecurity.None -> resources.getString(R.string.account_setup_outgoing_config_security_none)
- ConnectionSecurity.StartTLS -> resources.getString(R.string.account_setup_outgoing_config_security_start_tls)
- ConnectionSecurity.TLS -> resources.getString(R.string.account_setup_outgoing_config_security_ssl)
- }
-}
-
internal fun ValidationError.toResourceString(resources: Resources): String {
return when (this) {
is ValidateServerError -> toServerErrorString(resources)
diff --git a/feature/account/setup/src/main/res/values/strings.xml b/feature/account/setup/src/main/res/values/strings.xml
index b91b971b1..aca3f4e43 100644
--- a/feature/account/setup/src/main/res/values/strings.xml
+++ b/feature/account/setup/src/main/res/values/strings.xml
@@ -4,18 +4,25 @@
Next
Back
Finish
+ None
+ SSL/TLS
+ StartTLS
+ None available
K-9 Mail
Incoming server settings
+ Protocol
+ Server
+ Security
+ Auto-detect IMAP namespace
+ IMAP path prefix
+ Use compression
Outgoing server settings
Server
Server name is required.
Security
- None
- SSL/TLS
- StartTLS
Port
Port is required.
Port is invalid (must be 1–65535).
@@ -23,7 +30,6 @@
Username is required.
Password is required.
Client certificate
- None available
Auto-detect IMAP namespace
Use compression
diff --git a/feature/account/setup/src/test/kotlin/app/k9mail/feature/account/setup/domain/entity/ConnectionSecurityTest.kt b/feature/account/setup/src/test/kotlin/app/k9mail/feature/account/setup/domain/entity/ConnectionSecurityTest.kt
index 11f4e1424..4cd388642 100644
--- a/feature/account/setup/src/test/kotlin/app/k9mail/feature/account/setup/domain/entity/ConnectionSecurityTest.kt
+++ b/feature/account/setup/src/test/kotlin/app/k9mail/feature/account/setup/domain/entity/ConnectionSecurityTest.kt
@@ -1,36 +1,59 @@
package app.k9mail.feature.account.setup.domain.entity
+import assertk.assertThat
+import assertk.assertions.isEqualTo
import org.junit.Test
class ConnectionSecurityTest {
@Test
fun `should provide right default smtp port`() {
- val connectionSecurity = ConnectionSecurity.all()
+ val connectionSecurities = ConnectionSecurity.all()
- for (security in connectionSecurity) {
+ for (security in connectionSecurities) {
val port = security.toSmtpDefaultPort()
- when (security) {
- ConnectionSecurity.None -> assert(port == 587L)
- ConnectionSecurity.StartTLS -> assert(port == 587L)
- ConnectionSecurity.TLS -> assert(port == 465L)
- }
+ assertThat(port).isEqualTo(
+ when (security) {
+ ConnectionSecurity.None -> 587L
+ ConnectionSecurity.StartTLS -> 587L
+ ConnectionSecurity.TLS -> 465L
+ },
+ )
}
}
@Test
fun `should provide right default imap port`() {
- val connectionSecurity = ConnectionSecurity.all()
+ val connectionSecurities = ConnectionSecurity.all()
- for (security in connectionSecurity) {
+ for (security in connectionSecurities) {
val port = security.toImapDefaultPort()
- when (security) {
- ConnectionSecurity.None -> assert(port == 143L)
- ConnectionSecurity.StartTLS -> assert(port == 143L)
- ConnectionSecurity.TLS -> assert(port == 993L)
- }
+ assertThat(port).isEqualTo(
+ when (security) {
+ ConnectionSecurity.None -> 143L
+ ConnectionSecurity.StartTLS -> 143L
+ ConnectionSecurity.TLS -> 993L
+ },
+ )
+ }
+ }
+
+ @Test
+ fun `should provide right default pop3 port`() {
+ val connectionSecurities = ConnectionSecurity.all()
+
+ for (security in connectionSecurities) {
+ val port = security.toPop3DefaultPort()
+
+ assertThat(port).isEqualTo(
+ when (security) {
+ ConnectionSecurity.None -> 110L
+ ConnectionSecurity.StartTLS -> 110L
+ ConnectionSecurity.TLS -> 995L
+ },
+ )
}
}
}
diff --git a/feature/account/setup/src/test/kotlin/app/k9mail/feature/account/setup/domain/entity/IncomingProtocolTypeTest.kt b/feature/account/setup/src/test/kotlin/app/k9mail/feature/account/setup/domain/entity/IncomingProtocolTypeTest.kt
new file mode 100644
index 000000000..4931c1953
--- /dev/null
+++ b/feature/account/setup/src/test/kotlin/app/k9mail/feature/account/setup/domain/entity/IncomingProtocolTypeTest.kt
@@ -0,0 +1,40 @@
+package app.k9mail.feature.account.setup.domain.entity
+
+import assertk.assertThat
+import assertk.assertions.isEqualTo
+import org.junit.Test
+
+class IncomingProtocolTypeTest {
+
+ @Test
+ fun `should provide right default security`() {
+ val incomingProtocolTypes = IncomingProtocolType.all()
+
+ for (incomingProtocolType in incomingProtocolTypes) {
+ val security = incomingProtocolType.toDefaultSecurity()
+
+ assertThat(security).isEqualTo(
+ when (incomingProtocolType) {
+ IncomingProtocolType.IMAP -> ConnectionSecurity.TLS
+ IncomingProtocolType.POP3 -> ConnectionSecurity.TLS
+ },
+ )
+ }
+ }
+
+ @Test
+ fun `should provide right default port`() {
+ val incomingProtocolTypes = IncomingProtocolType.all()
+
+ for (incomingProtocolType in incomingProtocolTypes) {
+ val port = incomingProtocolType.toDefaultPort(ConnectionSecurity.TLS)
+
+ assertThat(port).isEqualTo(
+ when (incomingProtocolType) {
+ IncomingProtocolType.IMAP -> 993L
+ IncomingProtocolType.POP3 -> 995L
+ },
+ )
+ }
+ }
+}
diff --git a/feature/account/setup/src/test/kotlin/app/k9mail/feature/account/setup/domain/input/InputFieldTest.kt b/feature/account/setup/src/test/kotlin/app/k9mail/feature/account/setup/domain/input/InputFieldTest.kt
index c5c49b321..4d7c090ff 100644
--- a/feature/account/setup/src/test/kotlin/app/k9mail/feature/account/setup/domain/input/InputFieldTest.kt
+++ b/feature/account/setup/src/test/kotlin/app/k9mail/feature/account/setup/domain/input/InputFieldTest.kt
@@ -7,7 +7,9 @@ import assertk.all
import assertk.assertThat
import assertk.assertions.isEqualTo
import assertk.assertions.isFalse
+import assertk.assertions.isNotSameAs
import assertk.assertions.isNull
+import assertk.assertions.isSameAs
import assertk.assertions.isTrue
import assertk.assertions.prop
import org.junit.Test
@@ -27,7 +29,7 @@ data class InputFieldTestData(
@RunWith(Parameterized::class)
class InputFieldTest(
- val data: InputFieldTestData,
+ private val data: InputFieldTestData,
) {
@Test
@@ -50,6 +52,7 @@ class InputFieldTest(
val result = initialInput.updateValue(data.updatedValue)
assertThat(result).all {
+ isNotSameAs(initialInput)
hasValue(data.updatedValue)
hasNoError()
isNotValid()
@@ -67,6 +70,7 @@ class InputFieldTest(
val result = initialInput.updateError(TestValidationError)
assertThat(result).all {
+ isNotSameAs(initialInput)
hasValue(data.initialValue)
hasError(TestValidationError)
isNotValid()
@@ -84,6 +88,7 @@ class InputFieldTest(
val result = initialInput.updateValidity(isValid = true)
assertThat(result).all {
+ isNotSameAs(initialInput)
hasValue(data.initialValue)
hasNoError()
isValid()
@@ -101,6 +106,7 @@ class InputFieldTest(
val result = initialInput.updateValidity(isValid = false)
assertThat(result).all {
+ isSameAs(initialInput)
hasValue(data.initialValue)
hasError(TestValidationError)
isNotValid()
@@ -118,6 +124,7 @@ class InputFieldTest(
val result = initialInput.updateError(TestValidationError2)
assertThat(result).all {
+ isNotSameAs(initialInput)
hasValue(data.initialValue)
hasError(TestValidationError2)
isNotValid()
@@ -135,6 +142,7 @@ class InputFieldTest(
val result = initialInput.updateFromValidationResult(ValidationResult.Success)
assertThat(result).all {
+ isNotSameAs(initialInput)
hasValue(data.initialValue)
hasNoError()
isValid()
@@ -152,12 +160,45 @@ class InputFieldTest(
val result = initialInput.updateFromValidationResult(ValidationResult.Failure(TestValidationError))
assertThat(result).all {
+ isNotSameAs(initialInput)
hasValue(data.initialValue)
hasError(TestValidationError)
isNotValid()
}
}
+ @Test
+ fun `should decide equality on properties`() {
+ val input1 = data.createInitialInput(
+ data.initialValue,
+ data.initialError,
+ data.initialIsValid,
+ )
+ val input2 = data.createInitialInput(
+ data.initialValue,
+ data.initialError,
+ data.initialIsValid,
+ )
+
+ assertThat(input1.equals(input2)).isTrue()
+ }
+
+ @Test
+ fun `should have same hashCode`() {
+ val input1 = data.createInitialInput(
+ data.initialValue,
+ data.initialError,
+ data.initialIsValid,
+ )
+ val input2 = data.createInitialInput(
+ data.initialValue,
+ data.initialError,
+ data.initialIsValid,
+ )
+
+ assertThat(input1.hashCode()).isEqualTo(input2.hashCode())
+ }
+
private fun Assert>.hasValue(value: Any) {
prop("value") { InputField<*>::value.call(it) }.isEqualTo(value)
}
diff --git a/feature/account/setup/src/test/kotlin/app/k9mail/feature/account/setup/ui/AccountSetupScreenKtTest.kt b/feature/account/setup/src/test/kotlin/app/k9mail/feature/account/setup/ui/AccountSetupScreenKtTest.kt
index c67938ed7..ab534e754 100644
--- a/feature/account/setup/src/test/kotlin/app/k9mail/feature/account/setup/ui/AccountSetupScreenKtTest.kt
+++ b/feature/account/setup/src/test/kotlin/app/k9mail/feature/account/setup/ui/AccountSetupScreenKtTest.kt
@@ -7,6 +7,7 @@ import app.k9mail.core.ui.compose.theme.ThunderbirdTheme
import app.k9mail.feature.account.setup.ui.AccountSetupContract.Effect
import app.k9mail.feature.account.setup.ui.AccountSetupContract.SetupStep
import app.k9mail.feature.account.setup.ui.AccountSetupContract.State
+import app.k9mail.feature.account.setup.ui.incoming.FakeAccountIncomingConfigViewModel
import app.k9mail.feature.account.setup.ui.options.FakeAccountOptionsViewModel
import app.k9mail.feature.account.setup.ui.outgoing.FakeAccountOutgoingConfigViewModel
import assertk.assertThat
@@ -21,6 +22,7 @@ class AccountSetupScreenKtTest : ComposeTest() {
@Test
fun `should display correct screen for every setup step`() = runTest {
val viewModel = FakeAccountSetupViewModel()
+ val incomingViewModel = FakeAccountIncomingConfigViewModel()
val outgoingViewModel = FakeAccountOutgoingConfigViewModel()
val optionsViewModel = FakeAccountOptionsViewModel()
@@ -30,6 +32,7 @@ class AccountSetupScreenKtTest : ComposeTest() {
onFinish = { },
onBack = { },
viewModel = viewModel,
+ incomingViewModel = incomingViewModel,
outgoingViewModel = outgoingViewModel,
optionsViewModel = optionsViewModel,
)
@@ -46,6 +49,7 @@ class AccountSetupScreenKtTest : ComposeTest() {
fun `should delegate navigation effects`() = runTest {
val initialState = State()
val viewModel = FakeAccountSetupViewModel(initialState)
+ val incomingViewModel = FakeAccountIncomingConfigViewModel()
val outgoingViewModel = FakeAccountOutgoingConfigViewModel()
val optionsViewModel = FakeAccountOptionsViewModel()
var onFinishCounter = 0
@@ -57,6 +61,7 @@ class AccountSetupScreenKtTest : ComposeTest() {
onFinish = { onFinishCounter++ },
onBack = { onBackCounter++ },
viewModel = viewModel,
+ incomingViewModel = incomingViewModel,
outgoingViewModel = outgoingViewModel,
optionsViewModel = optionsViewModel,
)
diff --git a/feature/account/setup/src/test/kotlin/app/k9mail/feature/account/setup/ui/incoming/AccountIncomingConfigScreenKtTest.kt b/feature/account/setup/src/test/kotlin/app/k9mail/feature/account/setup/ui/incoming/AccountIncomingConfigScreenKtTest.kt
new file mode 100644
index 000000000..d00af1869
--- /dev/null
+++ b/feature/account/setup/src/test/kotlin/app/k9mail/feature/account/setup/ui/incoming/AccountIncomingConfigScreenKtTest.kt
@@ -0,0 +1,44 @@
+package app.k9mail.feature.account.setup.ui.incoming
+
+import app.k9mail.core.ui.compose.testing.ComposeTest
+import app.k9mail.core.ui.compose.testing.setContent
+import app.k9mail.feature.account.setup.ui.incoming.AccountIncomingConfigContract.Effect
+import app.k9mail.feature.account.setup.ui.incoming.AccountIncomingConfigContract.State
+import assertk.assertThat
+import assertk.assertions.isEqualTo
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+
+@OptIn(ExperimentalCoroutinesApi::class)
+class AccountIncomingConfigScreenKtTest : ComposeTest() {
+
+ @Test
+ fun `should delegate navigation effects`() = runTest {
+ val initialState = State()
+ val viewModel = FakeAccountIncomingConfigViewModel(initialState)
+ var onNextCounter = 0
+ var onBackCounter = 0
+
+ setContent {
+ AccountIncomingConfigScreen(
+ 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/setup/src/test/kotlin/app/k9mail/feature/account/setup/ui/incoming/AccountIncomingConfigStateTest.kt b/feature/account/setup/src/test/kotlin/app/k9mail/feature/account/setup/ui/incoming/AccountIncomingConfigStateTest.kt
new file mode 100644
index 000000000..666a0e12d
--- /dev/null
+++ b/feature/account/setup/src/test/kotlin/app/k9mail/feature/account/setup/ui/incoming/AccountIncomingConfigStateTest.kt
@@ -0,0 +1,34 @@
+package app.k9mail.feature.account.setup.ui.incoming
+
+import app.k9mail.feature.account.setup.domain.entity.ConnectionSecurity
+import app.k9mail.feature.account.setup.domain.entity.IncomingProtocolType
+import app.k9mail.feature.account.setup.domain.entity.toImapDefaultPort
+import app.k9mail.feature.account.setup.domain.input.NumberInputField
+import app.k9mail.feature.account.setup.domain.input.StringInputField
+import app.k9mail.feature.account.setup.ui.incoming.AccountIncomingConfigContract.State
+import assertk.all
+import assertk.assertThat
+import assertk.assertions.isEqualTo
+import assertk.assertions.isTrue
+import assertk.assertions.prop
+import org.junit.Test
+
+class AccountIncomingConfigStateTest {
+
+ @Test
+ fun `should set default values`() {
+ val state = State()
+
+ assertThat(state).all {
+ prop(State::protocolType).isEqualTo(IncomingProtocolType.IMAP)
+ prop(State::server).isEqualTo(StringInputField())
+ prop(State::security).isEqualTo(ConnectionSecurity.DEFAULT)
+ prop(State::port).isEqualTo(NumberInputField(value = ConnectionSecurity.DEFAULT.toImapDefaultPort()))
+ prop(State::username).isEqualTo(StringInputField())
+ prop(State::password).isEqualTo(StringInputField())
+ prop(State::clientCertificate).isEqualTo("")
+ prop(State::imapAutodetectNamespaceEnabled).isTrue()
+ prop(State::useCompression).isTrue()
+ }
+ }
+}
diff --git a/feature/account/setup/src/test/kotlin/app/k9mail/feature/account/setup/ui/incoming/AccountIncomingConfigViewModelTest.kt b/feature/account/setup/src/test/kotlin/app/k9mail/feature/account/setup/ui/incoming/AccountIncomingConfigViewModelTest.kt
new file mode 100644
index 000000000..b3df7de7b
--- /dev/null
+++ b/feature/account/setup/src/test/kotlin/app/k9mail/feature/account/setup/ui/incoming/AccountIncomingConfigViewModelTest.kt
@@ -0,0 +1,201 @@
+package app.k9mail.feature.account.setup.ui.incoming
+
+import androidx.lifecycle.viewmodel.compose.viewModel
+import app.cash.turbine.testIn
+import app.k9mail.core.ui.compose.testing.MainDispatcherRule
+import app.k9mail.feature.account.setup.domain.entity.ConnectionSecurity
+import app.k9mail.feature.account.setup.domain.entity.IncomingProtocolType
+import app.k9mail.feature.account.setup.domain.entity.toImapDefaultPort
+import app.k9mail.feature.account.setup.domain.entity.toPop3DefaultPort
+import app.k9mail.feature.account.setup.domain.input.NumberInputField
+import app.k9mail.feature.account.setup.domain.input.StringInputField
+import app.k9mail.feature.account.setup.testing.eventStateTest
+import app.k9mail.feature.account.setup.ui.incoming.AccountIncomingConfigContract.Effect
+import app.k9mail.feature.account.setup.ui.incoming.AccountIncomingConfigContract.Event
+import app.k9mail.feature.account.setup.ui.incoming.AccountIncomingConfigContract.State
+import assertk.assertions.assertThatAndTurbinesConsumed
+import assertk.assertions.isEqualTo
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import org.junit.Rule
+import org.junit.Test
+
+@OptIn(ExperimentalCoroutinesApi::class)
+class AccountIncomingConfigViewModelTest {
+
+ @get:Rule
+ val mainDispatcherRule = MainDispatcherRule()
+
+ private val testSubject = AccountIncomingConfigViewModel()
+
+ @Test
+ fun `should change protocol, security and port when ProtocolTypeChanged event is received`() = runTest {
+ val initialState = State(
+ security = ConnectionSecurity.StartTLS,
+ port = NumberInputField(value = ConnectionSecurity.StartTLS.toImapDefaultPort()),
+ )
+ testSubject.initState(initialState)
+
+ eventStateTest(
+ viewModel = testSubject,
+ initialState = initialState,
+ event = Event.ProtocolTypeChanged(IncomingProtocolType.POP3),
+ expectedState = State(
+ protocolType = IncomingProtocolType.POP3,
+ security = ConnectionSecurity.TLS,
+ port = NumberInputField(value = ConnectionSecurity.TLS.toPop3DefaultPort()),
+ ),
+ coroutineScope = backgroundScope,
+ )
+ }
+
+ @Test
+ fun `should change state when ServerChanged event is received`() = runTest {
+ eventStateTest(
+ viewModel = testSubject,
+ initialState = State(),
+ event = Event.ServerChanged("server"),
+ expectedState = State(server = StringInputField(value = "server")),
+ coroutineScope = backgroundScope,
+ )
+ }
+
+ @Test
+ fun `should change security and port when SecurityChanged event is received`() = runTest {
+ eventStateTest(
+ viewModel = testSubject,
+ initialState = State(),
+ event = Event.SecurityChanged(ConnectionSecurity.StartTLS),
+ expectedState = State(
+ security = ConnectionSecurity.StartTLS,
+ port = NumberInputField(value = ConnectionSecurity.StartTLS.toImapDefaultPort()),
+ ),
+ coroutineScope = backgroundScope,
+ )
+ }
+
+ @Test
+ fun `should change state when PortChanged event is received`() = runTest {
+ eventStateTest(
+ viewModel = testSubject,
+ initialState = State(),
+ event = Event.PortChanged(456L),
+ expectedState = State(port = NumberInputField(value = 456L)),
+ coroutineScope = backgroundScope,
+ )
+ }
+
+ @Test
+ fun `should change state when UsernameChanged event is received`() = runTest {
+ eventStateTest(
+ viewModel = testSubject,
+ initialState = State(),
+ event = Event.UsernameChanged("username"),
+ expectedState = State(username = StringInputField(value = "username")),
+ coroutineScope = backgroundScope,
+ )
+ }
+
+ @Test
+ fun `should change state when PasswordChanged event is received`() = runTest {
+ eventStateTest(
+ viewModel = testSubject,
+ initialState = State(),
+ event = Event.PasswordChanged("password"),
+ expectedState = State(password = StringInputField(value = "password")),
+ coroutineScope = backgroundScope,
+ )
+ }
+
+ @Test
+ fun `should change state when ClientCertificateChanged event is received`() = runTest {
+ eventStateTest(
+ viewModel = testSubject,
+ initialState = State(),
+ event = Event.ClientCertificateChanged("clientCertificate"),
+ expectedState = State(clientCertificate = "clientCertificate"),
+ coroutineScope = backgroundScope,
+ )
+ }
+
+ @Test
+ fun `should change state when ImapAutoDetectNamespaceChanged event is received`() = runTest {
+ eventStateTest(
+ viewModel = testSubject,
+ initialState = State(imapAutodetectNamespaceEnabled = true),
+ event = Event.ImapAutoDetectNamespaceChanged(false),
+ expectedState = State(imapAutodetectNamespaceEnabled = false),
+ coroutineScope = backgroundScope,
+ )
+ }
+
+ @Test
+ fun `should change state when ImapPrefixChanged event is received`() = runTest {
+ eventStateTest(
+ viewModel = testSubject,
+ initialState = State(),
+ event = Event.ImapPrefixChanged("imapPrefix"),
+ expectedState = State(imapPrefix = StringInputField(value = "imapPrefix")),
+ coroutineScope = backgroundScope,
+ )
+ }
+
+ @Test
+ fun `should change state when UseCompressionChanged event is received`() = runTest {
+ eventStateTest(
+ viewModel = testSubject,
+ initialState = State(useCompression = true),
+ event = Event.UseCompressionChanged(false),
+ expectedState = State(useCompression = false),
+ coroutineScope = backgroundScope,
+ )
+ }
+
+ @Test
+ fun `should emit NavigateNext effect when OnNextClicked event received`() = runTest {
+ val viewModel = testSubject
+ val stateTurbine = viewModel.state.testIn(backgroundScope)
+ val effectTurbine = viewModel.effect.testIn(backgroundScope)
+ val turbines = listOf(stateTurbine, effectTurbine)
+
+ assertThatAndTurbinesConsumed(
+ actual = stateTurbine.awaitItem(),
+ turbines = turbines,
+ ) {
+ isEqualTo(State())
+ }
+
+ viewModel.event(Event.OnNextClicked)
+
+ assertThatAndTurbinesConsumed(
+ actual = effectTurbine.awaitItem(),
+ turbines = turbines,
+ ) {
+ isEqualTo(Effect.NavigateNext)
+ }
+ }
+
+ @Test
+ fun `should emit NavigateBack effect when OnBackClicked event received`() = runTest {
+ val viewModel = testSubject
+ val stateTurbine = viewModel.state.testIn(backgroundScope)
+ val effectTurbine = viewModel.effect.testIn(backgroundScope)
+ val turbines = listOf(stateTurbine, effectTurbine)
+
+ assertThatAndTurbinesConsumed(
+ actual = stateTurbine.awaitItem(),
+ turbines = turbines,
+ ) {
+ isEqualTo(State())
+ }
+
+ viewModel.event(Event.OnBackClicked)
+
+ assertThatAndTurbinesConsumed(
+ actual = effectTurbine.awaitItem(),
+ turbines = turbines,
+ ) {
+ isEqualTo(Effect.NavigateBack)
+ }
+ }
+}
diff --git a/feature/account/setup/src/test/kotlin/app/k9mail/feature/account/setup/ui/incoming/FakeAccountIncomingConfigViewModel.kt b/feature/account/setup/src/test/kotlin/app/k9mail/feature/account/setup/ui/incoming/FakeAccountIncomingConfigViewModel.kt
new file mode 100644
index 000000000..05c35e2d7
--- /dev/null
+++ b/feature/account/setup/src/test/kotlin/app/k9mail/feature/account/setup/ui/incoming/FakeAccountIncomingConfigViewModel.kt
@@ -0,0 +1,26 @@
+package app.k9mail.feature.account.setup.ui.incoming
+
+import app.k9mail.core.ui.compose.common.mvi.BaseViewModel
+import app.k9mail.feature.account.setup.ui.incoming.AccountIncomingConfigContract.Effect
+import app.k9mail.feature.account.setup.ui.incoming.AccountIncomingConfigContract.Event
+import app.k9mail.feature.account.setup.ui.incoming.AccountIncomingConfigContract.State
+import app.k9mail.feature.account.setup.ui.incoming.AccountIncomingConfigContract.ViewModel
+
+class FakeAccountIncomingConfigViewModel(
+ initialState: State = State(),
+) : BaseViewModel(initialState), ViewModel {
+
+ val events = mutableListOf()
+
+ override fun initState(state: State) {
+ updateState { state }
+ }
+
+ override fun event(event: Event) {
+ events.add(event)
+ }
+
+ fun effect(effect: Effect) {
+ emitEffect(effect)
+ }
+}
diff --git a/feature/account/setup/src/test/kotlin/app/k9mail/feature/account/setup/ui/outgoing/AccountOutgoingConfigScreenKtTest.kt b/feature/account/setup/src/test/kotlin/app/k9mail/feature/account/setup/ui/outgoing/AccountOutgoingConfigScreenKtTest.kt
index 0de96e850..0c0d7c18d 100644
--- a/feature/account/setup/src/test/kotlin/app/k9mail/feature/account/setup/ui/outgoing/AccountOutgoingConfigScreenKtTest.kt
+++ b/feature/account/setup/src/test/kotlin/app/k9mail/feature/account/setup/ui/outgoing/AccountOutgoingConfigScreenKtTest.kt
@@ -6,9 +6,11 @@ import app.k9mail.feature.account.setup.ui.outgoing.AccountOutgoingConfigContrac
import app.k9mail.feature.account.setup.ui.outgoing.AccountOutgoingConfigContract.State
import assertk.assertThat
import assertk.assertions.isEqualTo
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Test
+@OptIn(ExperimentalCoroutinesApi::class)
class AccountOutgoingConfigScreenKtTest : ComposeTest() {
@Test