diff --git a/app/k9mail/src/main/java/com/fsck/k9/account/AccountCreator.kt b/app/k9mail/src/main/java/com/fsck/k9/account/AccountCreator.kt index 34b371cc9..1602f8ca4 100644 --- a/app/k9mail/src/main/java/com/fsck/k9/account/AccountCreator.kt +++ b/app/k9mail/src/main/java/com/fsck/k9/account/AccountCreator.kt @@ -59,6 +59,8 @@ class AccountCreator( newAccount.outgoingServerSettings = account.outgoingServerSettings + newAccount.oAuthState = account.authorizationState + newAccount.name = account.options.accountName newAccount.senderName = account.options.displayName if (account.options.emailSignature != null) { diff --git a/app/k9mail/src/test/java/com/fsck/k9/DependencyInjectionTest.kt b/app/k9mail/src/test/java/com/fsck/k9/DependencyInjectionTest.kt index 6c0da20e6..a5d9fe91f 100644 --- a/app/k9mail/src/test/java/com/fsck/k9/DependencyInjectionTest.kt +++ b/app/k9mail/src/test/java/com/fsck/k9/DependencyInjectionTest.kt @@ -3,7 +3,10 @@ package com.fsck.k9 import android.view.ContextThemeWrapper import androidx.lifecycle.LifecycleOwner import androidx.work.WorkerParameters +import app.k9mail.feature.account.setup.domain.DomainContract.UseCase.ValidateServerSettings +import app.k9mail.feature.account.setup.ui.validation.AccountValidationContract import com.fsck.k9.job.MailSyncWorker +import com.fsck.k9.mail.oauth.AuthStateStorage import com.fsck.k9.ui.R import com.fsck.k9.ui.changelog.ChangeLogMode import com.fsck.k9.ui.changelog.ChangelogViewModel @@ -17,6 +20,7 @@ import org.junit.runner.RunWith import org.koin.core.annotation.KoinInternalApi import org.koin.core.logger.PrintLogger import org.koin.core.parameter.parametersOf +import org.koin.core.qualifier.named import org.koin.java.KoinJavaComponent import org.koin.test.AutoCloseKoinTest import org.koin.test.check.checkModules @@ -34,6 +38,7 @@ class DependencyInjectionTest : AutoCloseKoinTest() { on { lifecycle } doReturn mock() } private val autocryptTransferView = mock() + private val authStateStorage = mock() @KoinInternalApi @Test @@ -53,6 +58,9 @@ class DependencyInjectionTest : AutoCloseKoinTest() { withParameters(clazz = Class.forName("com.fsck.k9.view.K9WebViewClient").kotlin) { parametersOf(null, null) } + withParameter(named("incoming_validation")) { authStateStorage } + withParameter(named("outgoing_validation")) { authStateStorage } + withParameter { authStateStorage } } } } diff --git a/feature/account/oauth/build.gradle.kts b/feature/account/oauth/build.gradle.kts index 3827c35a7..4b73b20bb 100644 --- a/feature/account/oauth/build.gradle.kts +++ b/feature/account/oauth/build.gradle.kts @@ -19,6 +19,7 @@ android { dependencies { implementation(projects.core.ui.compose.designsystem) implementation(projects.core.common) + implementation(projects.mail.common) implementation(projects.feature.account.common) implementation(libs.appauth) diff --git a/feature/account/oauth/src/main/kotlin/app/k9mail/feature/account/oauth/AccountOAuthModule.kt b/feature/account/oauth/src/main/kotlin/app/k9mail/feature/account/oauth/AccountOAuthModule.kt index a003ed9d2..23f1b9079 100644 --- a/feature/account/oauth/src/main/kotlin/app/k9mail/feature/account/oauth/AccountOAuthModule.kt +++ b/feature/account/oauth/src/main/kotlin/app/k9mail/feature/account/oauth/AccountOAuthModule.kt @@ -9,10 +9,10 @@ import app.k9mail.feature.account.oauth.domain.usecase.CheckIsGoogleSignIn import app.k9mail.feature.account.oauth.domain.usecase.FinishOAuthSignIn import app.k9mail.feature.account.oauth.domain.usecase.GetOAuthRequestIntent import app.k9mail.feature.account.oauth.domain.usecase.SuggestServerName +import app.k9mail.feature.account.oauth.ui.AccountOAuthContract import app.k9mail.feature.account.oauth.ui.AccountOAuthViewModel import net.openid.appauth.AuthorizationService import org.koin.android.ext.koin.androidApplication -import org.koin.androidx.viewmodel.dsl.viewModel import org.koin.core.module.Module import org.koin.dsl.module @@ -48,7 +48,7 @@ val featureAccountOAuthModule: Module = module { factory { CheckIsGoogleSignIn() } - viewModel { + factory { AccountOAuthViewModel( getOAuthRequestIntent = get(), finishOAuthSignIn = get(), diff --git a/feature/account/oauth/src/main/kotlin/app/k9mail/feature/account/oauth/ui/AccountOAuthContent.kt b/feature/account/oauth/src/main/kotlin/app/k9mail/feature/account/oauth/ui/AccountOAuthContent.kt index d0ab7053b..798560d6a 100644 --- a/feature/account/oauth/src/main/kotlin/app/k9mail/feature/account/oauth/ui/AccountOAuthContent.kt +++ b/feature/account/oauth/src/main/kotlin/app/k9mail/feature/account/oauth/ui/AccountOAuthContent.kt @@ -1,6 +1,7 @@ package app.k9mail.feature.account.oauth.ui import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -8,16 +9,15 @@ 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.ErrorView +import app.k9mail.core.ui.compose.designsystem.molecule.LoadingView 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.ContentListView -import app.k9mail.feature.account.common.ui.item.ErrorItem -import app.k9mail.feature.account.common.ui.item.LoadingItem import app.k9mail.feature.account.oauth.R import app.k9mail.feature.account.oauth.ui.AccountOAuthContract.Event import app.k9mail.feature.account.oauth.ui.AccountOAuthContract.State -import app.k9mail.feature.account.oauth.ui.item.SignInItem +import app.k9mail.feature.account.oauth.ui.view.SignInView @Composable internal fun AccountOAuthContent( @@ -27,33 +27,27 @@ internal fun AccountOAuthContent( ) { val resources = LocalContext.current.resources - ContentListView( + Column( modifier = Modifier .testTag("AccountOAuthContent") .then(modifier), verticalArrangement = Arrangement.spacedBy(MainTheme.spacings.double, Alignment.CenterVertically), ) { if (state.isLoading) { - item(key = "loading") { - LoadingItem( - message = stringResource(id = R.string.account_oauth_loading_message), - ) - } + LoadingView( + message = stringResource(id = R.string.account_oauth_loading_message), + ) } else if (state.error != null) { - item(key = "error") { - ErrorItem( - title = stringResource(id = R.string.account_oauth_loading_error), - message = state.error.toResourceString(resources), - onRetry = { onEvent(Event.OnRetryClicked) }, - ) - } + ErrorView( + title = stringResource(id = R.string.account_oauth_loading_error), + message = state.error.toResourceString(resources), + onRetry = { onEvent(Event.OnRetryClicked) }, + ) } else { - item(key = "sign_in") { - SignInItem( - onSignInClick = { onEvent(Event.SignInClicked) }, - isGoogleSignIn = state.isGoogleSignIn, - ) - } + SignInView( + onSignInClick = { onEvent(Event.SignInClicked) }, + isGoogleSignIn = state.isGoogleSignIn, + ) } } } diff --git a/feature/account/oauth/src/main/kotlin/app/k9mail/feature/account/oauth/ui/DummyAccountOAuthViewModel.kt b/feature/account/oauth/src/main/kotlin/app/k9mail/feature/account/oauth/ui/DummyAccountOAuthViewModel.kt new file mode 100644 index 000000000..0027077bb --- /dev/null +++ b/feature/account/oauth/src/main/kotlin/app/k9mail/feature/account/oauth/ui/DummyAccountOAuthViewModel.kt @@ -0,0 +1,14 @@ +package app.k9mail.feature.account.oauth.ui + +import app.k9mail.core.ui.compose.common.mvi.BaseViewModel + +// Only used by @DevicePreviews functions +class DummyAccountOAuthViewModel : + BaseViewModel( + AccountOAuthContract.State(), + ), + AccountOAuthContract.ViewModel { + + override fun initState(state: AccountOAuthContract.State) = Unit + override fun event(event: AccountOAuthContract.Event) = Unit +} diff --git a/feature/account/oauth/src/main/kotlin/app/k9mail/feature/account/oauth/ui/item/SignInItem.kt b/feature/account/oauth/src/main/kotlin/app/k9mail/feature/account/oauth/ui/item/SignInItem.kt deleted file mode 100644 index 6dc0d4ebe..000000000 --- a/feature/account/oauth/src/main/kotlin/app/k9mail/feature/account/oauth/ui/item/SignInItem.kt +++ /dev/null @@ -1,23 +0,0 @@ -package app.k9mail.feature.account.oauth.ui.item - -import androidx.compose.foundation.lazy.LazyItemScope -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import app.k9mail.feature.account.common.ui.item.ListItem -import app.k9mail.feature.account.oauth.ui.view.SignInView - -@Composable -internal fun LazyItemScope.SignInItem( - onSignInClick: () -> Unit, - isGoogleSignIn: Boolean, - modifier: Modifier = Modifier, -) { - ListItem( - modifier = modifier, - ) { - SignInView( - onSignInClick = onSignInClick, - isGoogleSignIn = isGoogleSignIn, - ) - } -} 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 648761b8e..53d358565 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 @@ -3,6 +3,7 @@ package app.k9mail.feature.account.setup import app.k9mail.autodiscovery.api.AutoDiscoveryService import app.k9mail.autodiscovery.service.RealAutoDiscoveryService import app.k9mail.core.common.coreCommonModule +import app.k9mail.feature.account.oauth.featureAccountOAuthModule import app.k9mail.feature.account.setup.domain.DomainContract import app.k9mail.feature.account.setup.domain.usecase.CreateAccount import app.k9mail.feature.account.setup.domain.usecase.GetAutoDiscovery @@ -22,17 +23,20 @@ import app.k9mail.feature.account.setup.ui.outgoing.AccountOutgoingConfigValidat import app.k9mail.feature.account.setup.ui.outgoing.AccountOutgoingConfigViewModel import app.k9mail.feature.account.setup.ui.validation.AccountValidationContract import app.k9mail.feature.account.setup.ui.validation.AccountValidationViewModel +import app.k9mail.feature.account.setup.ui.validation.InMemoryAuthStateStorage +import com.fsck.k9.mail.oauth.AuthStateStorage import com.fsck.k9.mail.store.imap.ImapServerSettingsValidator import com.fsck.k9.mail.store.pop3.Pop3ServerSettingsValidator import com.fsck.k9.mail.transport.smtp.SmtpServerSettingsValidator import okhttp3.OkHttpClient import org.koin.androidx.viewmodel.dsl.viewModel import org.koin.core.module.Module +import org.koin.core.parameter.parametersOf import org.koin.core.qualifier.named import org.koin.dsl.module val featureAccountSetupModule: Module = module { - includes(coreCommonModule) + includes(coreCommonModule, featureAccountOAuthModule) single { OkHttpClient() @@ -51,8 +55,9 @@ val featureAccountSetupModule: Module = module { ) } - factory { + factory { (authStateStorage: AuthStateStorage) -> ValidateServerSettings( + authStateStorage = authStateStorage, imapValidator = ImapServerSettingsValidator( trustedSocketFactory = get(), oAuth2TokenProviderFactory = get(), @@ -80,20 +85,24 @@ val featureAccountSetupModule: Module = module { factory { AccountOptionsValidator() } viewModel { + val authStateStorage = InMemoryAuthStateStorage() + AccountSetupViewModel( createAccount = get(), autoDiscoveryViewModel = get(), incomingViewModel = get(), - incomingValidationViewModel = get(named(NAME_INCOMING_VALIDATION)), + incomingValidationViewModel = get(named(NAME_INCOMING_VALIDATION)) { parametersOf(authStateStorage) }, outgoingViewModel = get(), - outgoingValidationViewModel = get(named(NAME_OUTGOING_VALIDATION)), + outgoingValidationViewModel = get(named(NAME_OUTGOING_VALIDATION)) { parametersOf(authStateStorage) }, optionsViewModel = get(), + authStateStorage = authStateStorage, ) } factory { AccountAutoDiscoveryViewModel( validator = get(), getAutoDiscovery = get(), + oAuthViewModel = get(), ) } factory { @@ -102,8 +111,10 @@ val featureAccountSetupModule: Module = module { ) } factory(named(NAME_INCOMING_VALIDATION)) { + (authStateStorage: AuthStateStorage) -> + AccountValidationViewModel( - validateServerSettings = get(), + validateServerSettings = get { parametersOf(authStateStorage) }, initialState = AccountValidationContract.State( isIncomingValidation = true, ), @@ -115,8 +126,10 @@ val featureAccountSetupModule: Module = module { ) } factory(named(NAME_OUTGOING_VALIDATION)) { + (authStateStorage: AuthStateStorage) -> + AccountValidationViewModel( - validateServerSettings = get(), + validateServerSettings = get { parametersOf(authStateStorage) }, initialState = AccountValidationContract.State( isIncomingValidation = false, ), diff --git a/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/domain/DomainContract.kt b/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/domain/DomainContract.kt index 27afc24ec..9ff6b7706 100644 --- a/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/domain/DomainContract.kt +++ b/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/domain/DomainContract.kt @@ -22,6 +22,7 @@ interface DomainContract { emailAddress: String, incomingServerSettings: ServerSettings, outgoingServerSettings: ServerSettings, + authorizationState: String?, options: AccountOptions, ): String } diff --git a/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/domain/entity/Account.kt b/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/domain/entity/Account.kt index 8eb90c170..0edc11e48 100644 --- a/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/domain/entity/Account.kt +++ b/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/domain/entity/Account.kt @@ -6,5 +6,6 @@ data class Account( val emailAddress: String, val incomingServerSettings: ServerSettings, val outgoingServerSettings: ServerSettings, + val authorizationState: String?, val options: AccountOptions, ) diff --git a/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/domain/usecase/CreateAccount.kt b/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/domain/usecase/CreateAccount.kt index 5329bb479..bbcc1975a 100644 --- a/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/domain/usecase/CreateAccount.kt +++ b/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/domain/usecase/CreateAccount.kt @@ -14,12 +14,14 @@ class CreateAccount( emailAddress: String, incomingServerSettings: ServerSettings, outgoingServerSettings: ServerSettings, + authorizationState: String?, options: AccountOptions, ): String { val account = Account( emailAddress = emailAddress, incomingServerSettings = incomingServerSettings, outgoingServerSettings = outgoingServerSettings, + authorizationState = authorizationState, options = options, ) diff --git a/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/domain/usecase/ValidateServerSettings.kt b/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/domain/usecase/ValidateServerSettings.kt index d802e8270..0d5a8f4cc 100644 --- a/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/domain/usecase/ValidateServerSettings.kt +++ b/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/domain/usecase/ValidateServerSettings.kt @@ -2,6 +2,7 @@ package app.k9mail.feature.account.setup.domain.usecase import app.k9mail.feature.account.setup.domain.DomainContract.UseCase import com.fsck.k9.mail.ServerSettings +import com.fsck.k9.mail.oauth.AuthStateStorage import com.fsck.k9.mail.server.ServerSettingsValidationResult import com.fsck.k9.mail.server.ServerSettingsValidator import kotlinx.coroutines.CoroutineDispatcher @@ -9,20 +10,22 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext internal class ValidateServerSettings( + private val authStateStorage: AuthStateStorage, private val imapValidator: ServerSettingsValidator, private val pop3Validator: ServerSettingsValidator, private val smtpValidator: ServerSettingsValidator, private val coroutineDispatcher: CoroutineDispatcher = Dispatchers.IO, ) : UseCase.ValidateServerSettings { - override suspend fun execute(settings: ServerSettings): ServerSettingsValidationResult = - withContext(coroutineDispatcher) { - return@withContext when (settings.type) { - "imap" -> imapValidator.checkServerSettings(settings, authStateStorage = null) - "pop3" -> pop3Validator.checkServerSettings(settings, authStateStorage = null) - "smtp" -> smtpValidator.checkServerSettings(settings, authStateStorage = null) + override suspend fun execute(settings: ServerSettings): ServerSettingsValidationResult { + return withContext(coroutineDispatcher) { + when (settings.type) { + "imap" -> imapValidator.checkServerSettings(settings, authStateStorage) + "pop3" -> pop3Validator.checkServerSettings(settings, authStateStorage) + "smtp" -> smtpValidator.checkServerSettings(settings, authStateStorage) else -> { throw IllegalArgumentException("Unsupported server type: ${settings.type}") } } } + } } diff --git a/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/ui/AccountSetupViewModel.kt b/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/ui/AccountSetupViewModel.kt index 58e6aa278..b8908f22e 100644 --- a/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/ui/AccountSetupViewModel.kt +++ b/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/ui/AccountSetupViewModel.kt @@ -20,8 +20,10 @@ import app.k9mail.feature.account.setup.ui.outgoing.AccountOutgoingConfigContrac import app.k9mail.feature.account.setup.ui.outgoing.toServerSettings import app.k9mail.feature.account.setup.ui.outgoing.toValidationState import app.k9mail.feature.account.setup.ui.validation.AccountValidationContract +import com.fsck.k9.mail.oauth.AuthStateStorage import kotlinx.coroutines.launch +@Suppress("LongParameterList") class AccountSetupViewModel( private val createAccount: UseCase.CreateAccount, override val autoDiscoveryViewModel: AccountAutoDiscoveryContract.ViewModel, @@ -30,6 +32,7 @@ class AccountSetupViewModel( override val outgoingViewModel: AccountOutgoingConfigContract.ViewModel, override val outgoingValidationViewModel: AccountValidationContract.ViewModel, override val optionsViewModel: AccountOptionsContract.ViewModel, + private val authStateStorage: AuthStateStorage, initialState: State = State(), ) : BaseViewModel(initialState), AccountSetupContract.ViewModel { @@ -52,6 +55,7 @@ class AccountSetupViewModel( private fun onAutoDiscoveryFinished( autoDiscoveryState: AccountAutoDiscoveryContract.State, ) { + authStateStorage.updateAuthorizationState(autoDiscoveryState.authorizationState?.state) incomingViewModel.initState(autoDiscoveryState.toIncomingConfigState()) outgoingViewModel.initState(autoDiscoveryState.toOutgoingConfigState()) optionsViewModel.initState(autoDiscoveryState.toOptionsState()) @@ -122,6 +126,10 @@ class AccountSetupViewModel( } private fun changeToSetupStep(setupStep: SetupStep) { + if (setupStep == SetupStep.AUTO_CONFIG) { + authStateStorage.updateAuthorizationState(authorizationState = null) + } + updateState { it.copy( setupStep = setupStep, @@ -140,6 +148,7 @@ class AccountSetupViewModel( emailAddress = autoDiscoveryState.emailAddress.value, incomingServerSettings = incomingState.toServerSettings(), outgoingServerSettings = outgoingState.toServerSettings(), + authorizationState = authStateStorage.getAuthorizationState(), options = optionsState.toAccountOptions(), ) 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 a34069b84..81d70da54 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 @@ -20,6 +20,8 @@ import app.k9mail.core.ui.compose.theme.MainTheme import app.k9mail.core.ui.compose.theme.ThunderbirdTheme import app.k9mail.feature.account.common.ui.item.ErrorItem import app.k9mail.feature.account.common.ui.item.LoadingItem +import app.k9mail.feature.account.oauth.ui.AccountOAuthContract +import app.k9mail.feature.account.oauth.ui.DummyAccountOAuthViewModel import app.k9mail.feature.account.setup.R import app.k9mail.feature.account.setup.ui.autodiscovery.AccountAutoDiscoveryContract.Event import app.k9mail.feature.account.setup.ui.autodiscovery.AccountAutoDiscoveryContract.State @@ -29,6 +31,7 @@ import app.k9mail.feature.account.setup.ui.autodiscovery.item.contentItems internal fun AccountAutoDiscoveryContent( state: State, onEvent: (Event) -> Unit, + oAuthViewModel: AccountOAuthContract.ViewModel, contentPadding: PaddingValues, modifier: Modifier = Modifier, ) { @@ -65,6 +68,7 @@ internal fun AccountAutoDiscoveryContent( contentItems( state = state, onEvent = onEvent, + oAuthViewModel = oAuthViewModel, ) } } @@ -79,6 +83,7 @@ internal fun AccountAutoDiscoveryContentK9Preview() { state = State(), onEvent = {}, contentPadding = PaddingValues(), + oAuthViewModel = DummyAccountOAuthViewModel(), ) } } @@ -91,6 +96,7 @@ internal fun AccountAutoDiscoveryContentThunderbirdPreview() { state = State(), onEvent = {}, contentPadding = PaddingValues(), + oAuthViewModel = DummyAccountOAuthViewModel(), ) } } 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 8d537658a..41fae236c 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 @@ -3,6 +3,9 @@ package app.k9mail.feature.account.setup.ui.autodiscovery import app.k9mail.autodiscovery.api.AutoDiscoveryResult import app.k9mail.core.common.domain.usecase.validation.ValidationResult import app.k9mail.core.ui.compose.common.mvi.UnidirectionalViewModel +import app.k9mail.feature.account.oauth.domain.entity.AuthorizationState +import app.k9mail.feature.account.oauth.domain.entity.OAuthResult +import app.k9mail.feature.account.oauth.ui.AccountOAuthContract import app.k9mail.feature.account.setup.domain.input.BooleanInputField import app.k9mail.feature.account.setup.domain.input.StringInputField @@ -15,6 +18,8 @@ interface AccountAutoDiscoveryContract { } interface ViewModel : UnidirectionalViewModel { + val oAuthViewModel: AccountOAuthContract.ViewModel + fun initState(state: State) } @@ -24,6 +29,9 @@ interface AccountAutoDiscoveryContract { val password: StringInputField = StringInputField(), val autoDiscoverySettings: AutoDiscoveryResult.Settings? = null, val configurationApproved: BooleanInputField = BooleanInputField(), + val authorizationState: AuthorizationState? = null, + + val isSuccess: Boolean = false, val error: Error? = null, val isLoading: Boolean = false, ) @@ -32,6 +40,7 @@ interface AccountAutoDiscoveryContract { data class EmailAddressChanged(val emailAddress: String) : Event() data class PasswordChanged(val password: String) : Event() data class ConfigurationApprovalChanged(val confirmed: Boolean) : Event() + data class OnOAuthResult(val result: OAuthResult) : Event() object OnNextClicked : Event() object OnBackClicked : Event() @@ -40,9 +49,8 @@ interface AccountAutoDiscoveryContract { } sealed class Effect { - data class NavigateNext( - val isAutomaticConfig: Boolean, - ) : Effect() + data class NavigateNext(val isAutomaticConfig: Boolean) : Effect() + object NavigateBack : Effect() } diff --git a/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/ui/autodiscovery/AccountAutoDiscoveryScreen.kt b/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/ui/autodiscovery/AccountAutoDiscoveryScreen.kt index 8da7f714f..3799fb857 100644 --- a/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/ui/autodiscovery/AccountAutoDiscoveryScreen.kt +++ b/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/ui/autodiscovery/AccountAutoDiscoveryScreen.kt @@ -12,6 +12,7 @@ import app.k9mail.core.ui.compose.theme.K9Theme import app.k9mail.core.ui.compose.theme.ThunderbirdTheme import app.k9mail.feature.account.common.ui.AppTitleTopHeader import app.k9mail.feature.account.common.ui.WizardNavigationBar +import app.k9mail.feature.account.oauth.ui.DummyAccountOAuthViewModel import app.k9mail.feature.account.setup.R import app.k9mail.feature.account.setup.ui.autodiscovery.AccountAutoDiscoveryContract.Effect import app.k9mail.feature.account.setup.ui.autodiscovery.AccountAutoDiscoveryContract.Event @@ -55,6 +56,7 @@ internal fun AccountAutoDiscoveryScreen( AccountAutoDiscoveryContent( state = state.value, onEvent = { dispatch(it) }, + oAuthViewModel = viewModel.oAuthViewModel, contentPadding = innerPadding, ) } @@ -70,6 +72,7 @@ internal fun AccountAutoDiscoveryScreenK9Preview() { viewModel = AccountAutoDiscoveryViewModel( validator = AccountAutoDiscoveryValidator(), getAutoDiscovery = { AutoDiscoveryResult.NoUsableSettingsFound }, + oAuthViewModel = DummyAccountOAuthViewModel(), ), ) } @@ -85,6 +88,7 @@ internal fun AccountAutoDiscoveryScreenThunderbirdPreview() { viewModel = AccountAutoDiscoveryViewModel( validator = AccountAutoDiscoveryValidator(), getAutoDiscovery = { AutoDiscoveryResult.NoUsableSettingsFound }, + oAuthViewModel = DummyAccountOAuthViewModel(), ), ) } diff --git a/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/ui/autodiscovery/AccountAutoDiscoveryViewModel.kt b/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/ui/autodiscovery/AccountAutoDiscoveryViewModel.kt index 805a2f45b..293e6619e 100644 --- a/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/ui/autodiscovery/AccountAutoDiscoveryViewModel.kt +++ b/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/ui/autodiscovery/AccountAutoDiscoveryViewModel.kt @@ -2,9 +2,13 @@ package app.k9mail.feature.account.setup.ui.autodiscovery import androidx.lifecycle.viewModelScope import app.k9mail.autodiscovery.api.AutoDiscoveryResult +import app.k9mail.autodiscovery.api.ImapServerSettings import app.k9mail.core.common.domain.usecase.validation.ValidationResult import app.k9mail.core.ui.compose.common.mvi.BaseViewModel +import app.k9mail.feature.account.oauth.domain.entity.OAuthResult +import app.k9mail.feature.account.oauth.ui.AccountOAuthContract import app.k9mail.feature.account.setup.domain.DomainContract.UseCase +import app.k9mail.feature.account.setup.domain.entity.AutoDiscoveryAuthenticationType import app.k9mail.feature.account.setup.domain.input.StringInputField import app.k9mail.feature.account.setup.ui.autodiscovery.AccountAutoDiscoveryContract.ConfigStep import app.k9mail.feature.account.setup.ui.autodiscovery.AccountAutoDiscoveryContract.Effect @@ -12,7 +16,6 @@ import app.k9mail.feature.account.setup.ui.autodiscovery.AccountAutoDiscoveryCon import app.k9mail.feature.account.setup.ui.autodiscovery.AccountAutoDiscoveryContract.Event import app.k9mail.feature.account.setup.ui.autodiscovery.AccountAutoDiscoveryContract.State import app.k9mail.feature.account.setup.ui.autodiscovery.AccountAutoDiscoveryContract.Validator -import app.k9mail.feature.account.setup.ui.autodiscovery.AccountAutoDiscoveryContract.ViewModel import kotlinx.coroutines.launch @Suppress("TooManyFunctions") @@ -20,7 +23,8 @@ internal class AccountAutoDiscoveryViewModel( initialState: State = State(), private val validator: Validator, private val getAutoDiscovery: UseCase.GetAutoDiscovery, -) : BaseViewModel(initialState), ViewModel { + override val oAuthViewModel: AccountOAuthContract.ViewModel, +) : BaseViewModel(initialState), AccountAutoDiscoveryContract.ViewModel { override fun initState(state: State) { updateState { @@ -33,6 +37,7 @@ internal class AccountAutoDiscoveryViewModel( is Event.EmailAddressChanged -> changeEmailAddress(event.emailAddress) is Event.PasswordChanged -> changePassword(event.password) is Event.ConfigurationApprovalChanged -> changeConfigurationApproval(event.confirmed) + is Event.OnOAuthResult -> onOAuthResult(event.result) Event.OnNextClicked -> onNext() Event.OnBackClicked -> onBack() @@ -120,7 +125,7 @@ internal class AccountAutoDiscoveryViewModel( val result = getAutoDiscovery.execute(state.value.emailAddress.value) when (result) { - AutoDiscoveryResult.NoUsableSettingsFound -> updateAutoDiscoverySettings(null) + AutoDiscoveryResult.NoUsableSettingsFound -> updateNoSettingsFound() is AutoDiscoveryResult.Settings -> updateAutoDiscoverySettings(result) is AutoDiscoveryResult.NetworkError -> updateError(Error.NetworkError) is AutoDiscoveryResult.UnexpectedException -> updateError(Error.UnknownError) @@ -128,12 +133,36 @@ internal class AccountAutoDiscoveryViewModel( } } - private fun updateAutoDiscoverySettings(settings: AutoDiscoveryResult.Settings?) { + private fun updateNoSettingsFound() { + updateState { + it.copy( + isLoading = false, + autoDiscoverySettings = null, + configStep = ConfigStep.PASSWORD, + ) + } + } + + private fun updateAutoDiscoverySettings(settings: AutoDiscoveryResult.Settings) { + val imapServerSettings = settings.incomingServerSettings as ImapServerSettings + val isOAuth = imapServerSettings.authenticationTypes.first() == AutoDiscoveryAuthenticationType.OAuth2 + + if (isOAuth) { + oAuthViewModel.initState( + AccountOAuthContract.State( + hostname = imapServerSettings.hostname.value, + emailAddress = state.value.emailAddress.value, + ), + ) + } + + // TODO: disable next button if isOAuth = true + updateState { it.copy( isLoading = false, autoDiscoverySettings = settings, - configStep = ConfigStep.PASSWORD, // TODO use oauth if applicable + configStep = if (isOAuth) ConfigStep.OAUTH else ConfigStep.PASSWORD, ) } } @@ -189,14 +218,28 @@ internal class AccountAutoDiscoveryViewModel( } } - ConfigStep.PASSWORD -> updateState { + ConfigStep.OAUTH, + ConfigStep.PASSWORD, + -> updateState { it.copy( configStep = ConfigStep.EMAIL_ADDRESS, password = StringInputField(), ) } + } + } - ConfigStep.OAUTH -> TODO() + private fun onOAuthResult(result: OAuthResult) { + if (result is OAuthResult.Success) { + updateState { + it.copy(authorizationState = result.authorizationState) + } + + navigateNext(isAutomaticConfig = true) + } else { + updateState { + it.copy(authorizationState = null) + } } } diff --git a/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/ui/autodiscovery/item/ContentItems.kt b/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/ui/autodiscovery/item/ContentItems.kt index 29c5f07ae..fe9e48b7d 100644 --- a/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/ui/autodiscovery/item/ContentItems.kt +++ b/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/ui/autodiscovery/item/ContentItems.kt @@ -1,6 +1,9 @@ package app.k9mail.feature.account.setup.ui.autodiscovery.item import androidx.compose.foundation.lazy.LazyListScope +import app.k9mail.feature.account.common.ui.item.ListItem +import app.k9mail.feature.account.oauth.ui.AccountOAuthContract +import app.k9mail.feature.account.oauth.ui.AccountOAuthView import app.k9mail.feature.account.setup.ui.autodiscovery.AccountAutoDiscoveryContract.ConfigStep import app.k9mail.feature.account.setup.ui.autodiscovery.AccountAutoDiscoveryContract.Event import app.k9mail.feature.account.setup.ui.autodiscovery.AccountAutoDiscoveryContract.State @@ -8,8 +11,9 @@ import app.k9mail.feature.account.setup.ui.autodiscovery.AccountAutoDiscoveryCon internal fun LazyListScope.contentItems( state: State, onEvent: (Event) -> Unit, + oAuthViewModel: AccountOAuthContract.ViewModel, ) { - if (state.configStep == ConfigStep.PASSWORD) { + if (state.configStep != ConfigStep.EMAIL_ADDRESS) { item(key = "autodiscovery") { AutoDiscoveryStatusItem( autoDiscoverySettings = state.autoDiscoverySettings, @@ -43,5 +47,14 @@ internal fun LazyListScope.contentItems( onPasswordChange = { onEvent(Event.PasswordChanged(it)) }, ) } + } else if (state.configStep == ConfigStep.OAUTH) { + item(key = "oauth") { + ListItem { + AccountOAuthView( + onOAuthResult = { result -> onEvent(Event.OnOAuthResult(result)) }, + viewModel = oAuthViewModel, + ) + } + } } } diff --git a/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/ui/validation/AccountValidationViewModel.kt b/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/ui/validation/AccountValidationViewModel.kt index 4d1f5f806..9de7d93e3 100644 --- a/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/ui/validation/AccountValidationViewModel.kt +++ b/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/ui/validation/AccountValidationViewModel.kt @@ -7,7 +7,6 @@ import app.k9mail.feature.account.setup.ui.validation.AccountValidationContract. import app.k9mail.feature.account.setup.ui.validation.AccountValidationContract.Error import app.k9mail.feature.account.setup.ui.validation.AccountValidationContract.Event import app.k9mail.feature.account.setup.ui.validation.AccountValidationContract.State -import app.k9mail.feature.account.setup.ui.validation.AccountValidationContract.ViewModel import com.fsck.k9.mail.server.ServerSettingsValidationResult import kotlinx.coroutines.delay import kotlinx.coroutines.launch @@ -17,7 +16,7 @@ private const val CONTINUE_NEXT_DELAY = 2000L internal class AccountValidationViewModel( initialState: State = State(), private val validateServerSettings: DomainContract.UseCase.ValidateServerSettings, -) : BaseViewModel(initialState), ViewModel { +) : BaseViewModel(initialState), AccountValidationContract.ViewModel { override fun initState(state: State) { updateState { diff --git a/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/ui/validation/InMemoryAuthStateStorage.kt b/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/ui/validation/InMemoryAuthStateStorage.kt new file mode 100644 index 000000000..809d7922c --- /dev/null +++ b/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/ui/validation/InMemoryAuthStateStorage.kt @@ -0,0 +1,17 @@ +package app.k9mail.feature.account.setup.ui.validation + +import com.fsck.k9.mail.oauth.AuthStateStorage + +class InMemoryAuthStateStorage : AuthStateStorage { + private var authorizationState: String? = null + + @Synchronized + override fun getAuthorizationState(): String? { + return authorizationState + } + + @Synchronized + override fun updateAuthorizationState(authorizationState: String?) { + this.authorizationState = authorizationState + } +} diff --git a/feature/account/setup/src/test/kotlin/app/k9mail/feature/account/setup/AccountSetupModuleKtTest.kt b/feature/account/setup/src/test/kotlin/app/k9mail/feature/account/setup/AccountSetupModuleKtTest.kt index 0287162e6..7e17bd5b6 100644 --- a/feature/account/setup/src/test/kotlin/app/k9mail/feature/account/setup/AccountSetupModuleKtTest.kt +++ b/feature/account/setup/src/test/kotlin/app/k9mail/feature/account/setup/AccountSetupModuleKtTest.kt @@ -1,14 +1,19 @@ package app.k9mail.feature.account.setup +import android.content.Context import app.k9mail.core.common.oauth.OAuthConfigurationFactory +import app.k9mail.feature.account.oauth.ui.AccountOAuthContract import app.k9mail.feature.account.setup.AccountSetupExternalContract.AccountCreator import app.k9mail.feature.account.setup.AccountSetupExternalContract.AccountCreator.AccountCreatorResult +import app.k9mail.feature.account.setup.domain.DomainContract.UseCase.ValidateServerSettings import app.k9mail.feature.account.setup.ui.AccountSetupContract import app.k9mail.feature.account.setup.ui.autodiscovery.AccountAutoDiscoveryContract import app.k9mail.feature.account.setup.ui.incoming.AccountIncomingConfigContract import app.k9mail.feature.account.setup.ui.options.AccountOptionsContract import app.k9mail.feature.account.setup.ui.outgoing.AccountOutgoingConfigContract import app.k9mail.feature.account.setup.ui.validation.AccountValidationContract +import app.k9mail.feature.account.setup.ui.validation.InMemoryAuthStateStorage +import com.fsck.k9.mail.oauth.AuthStateStorage import com.fsck.k9.mail.oauth.OAuth2TokenProvider import com.fsck.k9.mail.oauth.OAuth2TokenProviderFactory import com.fsck.k9.mail.ssl.TrustedSocketFactory @@ -18,6 +23,7 @@ import org.junit.runner.RunWith import org.koin.android.ext.koin.androidContext import org.koin.core.annotation.KoinExperimentalAPI import org.koin.core.module.Module +import org.koin.core.qualifier.named import org.koin.dsl.koinApplication import org.koin.dsl.module import org.koin.test.KoinTest @@ -55,17 +61,26 @@ class AccountSetupModuleKtTest : KoinTest { extraTypes = listOf( AccountSetupContract.State::class, AccountAutoDiscoveryContract.State::class, + AccountOAuthContract.State::class, AccountValidationContract.State::class, AccountIncomingConfigContract.State::class, AccountOutgoingConfigContract.State::class, AccountOptionsContract.State::class, + AuthStateStorage::class, + Context::class, + Class.forName("net.openid.appauth.AppAuthConfiguration").kotlin, ), ) koinApplication { modules(externalModule, featureAccountSetupModule) androidContext(RuntimeEnvironment.getApplication()) - checkModules() + checkModules { + val authStateStorage = InMemoryAuthStateStorage() + withParameter(named(NAME_INCOMING_VALIDATION)) { authStateStorage } + withParameter(named(NAME_OUTGOING_VALIDATION)) { authStateStorage } + withParameter { authStateStorage } + } } } } diff --git a/feature/account/setup/src/test/kotlin/app/k9mail/feature/account/setup/domain/usecase/CreateAccountTest.kt b/feature/account/setup/src/test/kotlin/app/k9mail/feature/account/setup/domain/usecase/CreateAccountTest.kt index 4bb09548d..f6a53976b 100644 --- a/feature/account/setup/src/test/kotlin/app/k9mail/feature/account/setup/domain/usecase/CreateAccountTest.kt +++ b/feature/account/setup/src/test/kotlin/app/k9mail/feature/account/setup/domain/usecase/CreateAccountTest.kt @@ -44,6 +44,7 @@ class CreateAccountTest { password = "password", clientCertificateAlias = null, ) + val authorizationState = "authorization state" val options = AccountOptions( accountName = "accountName", displayName = "displayName", @@ -53,7 +54,13 @@ class CreateAccountTest { showNotification = true, ) - val result = createAccount.execute(emailAddress, incomingServerSettings, outgoingServerSettings, options) + val result = createAccount.execute( + emailAddress, + incomingServerSettings, + outgoingServerSettings, + authorizationState, + options, + ) assertThat(result).isEqualTo("uuid") assertThat(recordedAccount).isEqualTo( @@ -61,6 +68,7 @@ class CreateAccountTest { emailAddress = emailAddress, incomingServerSettings = incomingServerSettings, outgoingServerSettings = outgoingServerSettings, + authorizationState = authorizationState, options = options, ), ) diff --git a/feature/account/setup/src/test/kotlin/app/k9mail/feature/account/setup/domain/usecase/ValidateServerSettingsTest.kt b/feature/account/setup/src/test/kotlin/app/k9mail/feature/account/setup/domain/usecase/ValidateServerSettingsTest.kt index 0311bbc05..8fcec7ff2 100644 --- a/feature/account/setup/src/test/kotlin/app/k9mail/feature/account/setup/domain/usecase/ValidateServerSettingsTest.kt +++ b/feature/account/setup/src/test/kotlin/app/k9mail/feature/account/setup/domain/usecase/ValidateServerSettingsTest.kt @@ -1,5 +1,6 @@ package app.k9mail.feature.account.setup.domain.usecase +import app.k9mail.feature.account.setup.ui.validation.InMemoryAuthStateStorage import assertk.assertThat import assertk.assertions.isEqualTo import com.fsck.k9.mail.AuthType @@ -11,10 +12,12 @@ import kotlinx.coroutines.test.runTest import org.junit.Test class ValidateServerSettingsTest { + private val authStateStorage = InMemoryAuthStateStorage() @Test fun `should check with imap validator when protocol is imap`() = runTest { val testSubject = ValidateServerSettings( + authStateStorage = authStateStorage, imapValidator = { _, _ -> ServerSettingsValidationResult.Success }, pop3Validator = { _, _ -> ServerSettingsValidationResult.NetworkError(IOException("Failed POP3")) }, smtpValidator = { _, _ -> ServerSettingsValidationResult.NetworkError(IOException("Failed SMTP")) }, @@ -29,6 +32,7 @@ class ValidateServerSettingsTest { fun `should check with imap validator when protocol is imap and return failure`() = runTest { val failure = ServerSettingsValidationResult.ServerError("Failed") val testSubject = ValidateServerSettings( + authStateStorage = authStateStorage, imapValidator = { _, _ -> failure }, pop3Validator = { _, _ -> ServerSettingsValidationResult.NetworkError(IOException("Failed POP3")) }, smtpValidator = { _, _ -> ServerSettingsValidationResult.NetworkError(IOException("Failed SMTP")) }, @@ -42,6 +46,7 @@ class ValidateServerSettingsTest { @Test fun `should check with pop3 validator when protocol is pop3`() = runTest { val testSubject = ValidateServerSettings( + authStateStorage = authStateStorage, imapValidator = { _, _ -> ServerSettingsValidationResult.NetworkError(IOException("Failed IMAP")) }, pop3Validator = { _, _ -> ServerSettingsValidationResult.Success }, smtpValidator = { _, _ -> ServerSettingsValidationResult.NetworkError(IOException("Failed SMTP")) }, @@ -56,6 +61,7 @@ class ValidateServerSettingsTest { fun `should check with pop3 validator when protocol is pop3 and return failure`() = runTest { val failure = ServerSettingsValidationResult.ServerError("Failed POP3") val testSubject = ValidateServerSettings( + authStateStorage = authStateStorage, imapValidator = { _, _ -> ServerSettingsValidationResult.NetworkError(IOException("Failed IMAP")) }, pop3Validator = { _, _ -> failure }, smtpValidator = { _, _ -> ServerSettingsValidationResult.NetworkError(IOException("Failed SMTP")) }, @@ -69,6 +75,7 @@ class ValidateServerSettingsTest { @Test fun `should check with smtp validator when protocol is smtp`() = runTest { val testSubject = ValidateServerSettings( + authStateStorage = authStateStorage, imapValidator = { _, _ -> ServerSettingsValidationResult.NetworkError(IOException("Failed IMAP")) }, pop3Validator = { _, _ -> ServerSettingsValidationResult.NetworkError(IOException("Failed POP3")) }, smtpValidator = { _, _ -> ServerSettingsValidationResult.Success }, @@ -83,6 +90,7 @@ class ValidateServerSettingsTest { fun `should check with smtp validator when protocol is smtp and return failure`() = runTest { val failure = ServerSettingsValidationResult.ServerError("Failed SMTP") val testSubject = ValidateServerSettings( + authStateStorage = authStateStorage, imapValidator = { _, _ -> ServerSettingsValidationResult.NetworkError(IOException("Failed IMAP")) }, pop3Validator = { _, _ -> ServerSettingsValidationResult.NetworkError(IOException("Failed POP3")) }, smtpValidator = { _, _ -> failure }, diff --git a/feature/account/setup/src/test/kotlin/app/k9mail/feature/account/setup/ui/AccountSetupViewModelTest.kt b/feature/account/setup/src/test/kotlin/app/k9mail/feature/account/setup/ui/AccountSetupViewModelTest.kt index 6092c19bd..687aba0f5 100644 --- a/feature/account/setup/src/test/kotlin/app/k9mail/feature/account/setup/ui/AccountSetupViewModelTest.kt +++ b/feature/account/setup/src/test/kotlin/app/k9mail/feature/account/setup/ui/AccountSetupViewModelTest.kt @@ -31,9 +31,11 @@ import app.k9mail.feature.account.setup.ui.outgoing.FakeAccountOutgoingConfigVie import app.k9mail.feature.account.setup.ui.outgoing.toServerSettings import app.k9mail.feature.account.setup.ui.outgoing.toValidationState import app.k9mail.feature.account.setup.ui.validation.FakeAccountValidationViewModel +import app.k9mail.feature.account.setup.ui.validation.InMemoryAuthStateStorage import assertk.assertThat import assertk.assertions.assertThatAndTurbinesConsumed import assertk.assertions.isEqualTo +import assertk.assertions.isNull import assertk.assertions.prop import com.fsck.k9.mail.ServerSettings import kotlinx.coroutines.test.runTest @@ -54,18 +56,21 @@ class AccountSetupViewModelTest { private val outgoingViewModel = FakeAccountOutgoingConfigViewModel() private val outgoingValidationViewModel = FakeAccountValidationViewModel() private val optionsViewModel = FakeAccountOptionsViewModel() + private val authStateStorage = InMemoryAuthStateStorage() @Test fun `should forward step state on next event`() = runTest { var createAccountEmailAddress: String? = null var createAccountIncomingServerSettings: ServerSettings? = null var createAccountOutgoingServerSettings: ServerSettings? = null + var createAccountAuthorizationState: String? = null var createAccountOptions: AccountOptions? = null val viewModel = AccountSetupViewModel( - createAccount = { emailAddress, incomingServerSettings, outgoingServerSettings, options -> + createAccount = { emailAddress, incomingServerSettings, outgoingServerSettings, authState, options -> createAccountEmailAddress = emailAddress createAccountIncomingServerSettings = incomingServerSettings createAccountOutgoingServerSettings = outgoingServerSettings + createAccountAuthorizationState = authState createAccountOptions = options "accountUuid" @@ -76,6 +81,7 @@ class AccountSetupViewModelTest { outgoingViewModel = outgoingViewModel, outgoingValidationViewModel = outgoingValidationViewModel, optionsViewModel = optionsViewModel, + authStateStorage = authStateStorage, ) val stateTurbine = viewModel.state.testIn(backgroundScope) val effectTurbine = viewModel.effect.testIn(backgroundScope) @@ -194,6 +200,7 @@ class AccountSetupViewModelTest { assertThat(createAccountEmailAddress).isEqualTo(EMAIL_ADDRESS) assertThat(createAccountIncomingServerSettings).isEqualTo(expectedIncomingConfigState.toServerSettings()) assertThat(createAccountOutgoingServerSettings).isEqualTo(expectedOutgoingConfigState.toServerSettings()) + assertThat(createAccountAuthorizationState).isNull() assertThat(createAccountOptions).isEqualTo( AccountOptions( accountName = "account name", @@ -210,13 +217,14 @@ class AccountSetupViewModelTest { fun `should rewind step state on back event`() = runTest { val initialState = State(setupStep = SetupStep.OPTIONS) val viewModel = AccountSetupViewModel( - createAccount = { _, _, _, _ -> "accountUuid" }, + createAccount = { _, _, _, _, _ -> "accountUuid" }, autoDiscoveryViewModel = autoDiscoveryViewModel, incomingViewModel = FakeAccountIncomingConfigViewModel(), incomingValidationViewModel = FakeAccountValidationViewModel(), outgoingViewModel = FakeAccountOutgoingConfigViewModel(), outgoingValidationViewModel = FakeAccountValidationViewModel(), optionsViewModel = FakeAccountOptionsViewModel(), + authStateStorage = authStateStorage, initialState = initialState, ) val stateTurbine = viewModel.state.testIn(backgroundScope) diff --git a/feature/account/setup/src/test/kotlin/app/k9mail/feature/account/setup/ui/autodiscovery/AccountAutoDiscoveryViewModelTest.kt b/feature/account/setup/src/test/kotlin/app/k9mail/feature/account/setup/ui/autodiscovery/AccountAutoDiscoveryViewModelTest.kt index 2d990164d..211dcfd41 100644 --- a/feature/account/setup/src/test/kotlin/app/k9mail/feature/account/setup/ui/autodiscovery/AccountAutoDiscoveryViewModelTest.kt +++ b/feature/account/setup/src/test/kotlin/app/k9mail/feature/account/setup/ui/autodiscovery/AccountAutoDiscoveryViewModelTest.kt @@ -33,6 +33,7 @@ class AccountAutoDiscoveryViewModelTest { delay(50) AutoDiscoveryResult.NoUsableSettingsFound }, + oAuthViewModel = FakeAccountOAuthViewModel(), ) @Test @@ -97,6 +98,7 @@ class AccountAutoDiscoveryViewModelTest { delay(50) autoDiscoverySettings }, + oAuthViewModel = FakeAccountOAuthViewModel(), initialState = initialState, ) val stateTurbine = viewModel.state.testIn(backgroundScope) @@ -153,6 +155,7 @@ class AccountAutoDiscoveryViewModelTest { delay(50) AutoDiscoveryResult.UnexpectedException(discoveryError) }, + oAuthViewModel = FakeAccountOAuthViewModel(), initialState = initialState, ) val stateTurbine = viewModel.state.testIn(backgroundScope) @@ -234,6 +237,7 @@ class AccountAutoDiscoveryViewModelTest { emailAddressAnswer = ValidationResult.Failure(TestError), ), getAutoDiscovery = { AutoDiscoveryResult.NoUsableSettingsFound }, + oAuthViewModel = FakeAccountOAuthViewModel(), initialState = initialState, ) @@ -317,6 +321,7 @@ class AccountAutoDiscoveryViewModelTest { passwordAnswer = ValidationResult.Failure(TestError), ), getAutoDiscovery = { AutoDiscoveryResult.NoUsableSettingsFound }, + oAuthViewModel = FakeAccountOAuthViewModel(), initialState = initialState, ) val stateTurbine = viewModel.state.testIn(backgroundScope) diff --git a/feature/account/setup/src/test/kotlin/app/k9mail/feature/account/setup/ui/autodiscovery/FakeAccountAutoDiscoveryViewModel.kt b/feature/account/setup/src/test/kotlin/app/k9mail/feature/account/setup/ui/autodiscovery/FakeAccountAutoDiscoveryViewModel.kt index 8e4a94f10..5155f6c42 100644 --- a/feature/account/setup/src/test/kotlin/app/k9mail/feature/account/setup/ui/autodiscovery/FakeAccountAutoDiscoveryViewModel.kt +++ b/feature/account/setup/src/test/kotlin/app/k9mail/feature/account/setup/ui/autodiscovery/FakeAccountAutoDiscoveryViewModel.kt @@ -1,17 +1,19 @@ package app.k9mail.feature.account.setup.ui.autodiscovery import app.k9mail.core.ui.compose.common.mvi.BaseViewModel +import app.k9mail.feature.account.oauth.ui.AccountOAuthContract import app.k9mail.feature.account.setup.ui.autodiscovery.AccountAutoDiscoveryContract.Effect import app.k9mail.feature.account.setup.ui.autodiscovery.AccountAutoDiscoveryContract.Event import app.k9mail.feature.account.setup.ui.autodiscovery.AccountAutoDiscoveryContract.State -import app.k9mail.feature.account.setup.ui.autodiscovery.AccountAutoDiscoveryContract.ViewModel class FakeAccountAutoDiscoveryViewModel( initialState: State = State(), -) : BaseViewModel(initialState), ViewModel { +) : BaseViewModel(initialState), AccountAutoDiscoveryContract.ViewModel { val events = mutableListOf() + override val oAuthViewModel: AccountOAuthContract.ViewModel = FakeAccountOAuthViewModel() + override fun initState(state: State) { updateState { state } } diff --git a/feature/account/setup/src/test/kotlin/app/k9mail/feature/account/setup/ui/autodiscovery/FakeAccountOAuthViewModel.kt b/feature/account/setup/src/test/kotlin/app/k9mail/feature/account/setup/ui/autodiscovery/FakeAccountOAuthViewModel.kt new file mode 100644 index 000000000..13e3c7e9e --- /dev/null +++ b/feature/account/setup/src/test/kotlin/app/k9mail/feature/account/setup/ui/autodiscovery/FakeAccountOAuthViewModel.kt @@ -0,0 +1,26 @@ +package app.k9mail.feature.account.setup.ui.autodiscovery + +import app.k9mail.core.ui.compose.common.mvi.BaseViewModel +import app.k9mail.feature.account.oauth.ui.AccountOAuthContract +import app.k9mail.feature.account.oauth.ui.AccountOAuthContract.Effect +import app.k9mail.feature.account.oauth.ui.AccountOAuthContract.Event +import app.k9mail.feature.account.oauth.ui.AccountOAuthContract.State + +internal class FakeAccountOAuthViewModel( + initialState: State = State(), +) : BaseViewModel(initialState), AccountOAuthContract.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/mail/common/src/main/java/com/fsck/k9/mail/ServerSettings.kt b/mail/common/src/main/java/com/fsck/k9/mail/ServerSettings.kt index 9be006f67..28707a732 100644 --- a/mail/common/src/main/java/com/fsck/k9/mail/ServerSettings.kt +++ b/mail/common/src/main/java/com/fsck/k9/mail/ServerSettings.kt @@ -16,6 +16,7 @@ data class ServerSettings @JvmOverloads constructor( ) { val isMissingCredentials: Boolean = when (authenticationType) { AuthType.EXTERNAL -> clientCertificateAlias == null + AuthType.XOAUTH2 -> username.isBlank() else -> username.isNotBlank() && password == null }