Skip to content
Merged
1 change: 1 addition & 0 deletions feature/account/oauth/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ dependencies {
implementation(libs.appauth)
implementation(libs.androidx.compose.material3)

testImplementation(projects.core.logging.testing)
testImplementation(projects.core.ui.compose.testing)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ val featureAccountOAuthModule: Module = module {
getOAuthRequestIntent = get(),
finishOAuthSignIn = get(),
checkIsGoogleSignIn = get(),
logger = get(),
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import android.content.Intent

sealed interface AuthorizationIntentResult {
object NotSupported : AuthorizationIntentResult
object BrowserNotAvailable : AuthorizationIntentResult

data class Success(
val intent: Intent,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package app.k9mail.feature.account.oauth.ui

import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Intent
import androidx.lifecycle.viewModelScope
import app.k9mail.feature.account.common.domain.entity.AuthorizationState
Expand All @@ -13,13 +14,15 @@ 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.AccountOAuthContract.ViewModel
import kotlinx.coroutines.launch
import net.thunderbird.core.logging.Logger
import net.thunderbird.core.ui.contract.mvi.BaseViewModel

class AccountOAuthViewModel(
initialState: State = State(),
private val getOAuthRequestIntent: UseCase.GetOAuthRequestIntent,
private val finishOAuthSignIn: UseCase.FinishOAuthSignIn,
private val checkIsGoogleSignIn: UseCase.CheckIsGoogleSignIn,
private val logger: Logger,
) : BaseViewModel<State, Event, Effect>(initialState), ViewModel {

override fun initState(state: State) {
Expand All @@ -45,10 +48,15 @@ class AccountOAuthViewModel(
}

private fun onSignIn() {
val result = getOAuthRequestIntent.execute(
hostname = state.value.hostname,
emailAddress = state.value.emailAddress,
)
val result = try {
getOAuthRequestIntent.execute(
hostname = state.value.hostname,
emailAddress = state.value.emailAddress,
)
} catch (e: ActivityNotFoundException) {
logger.error(throwable = e) { "Failed to launch custom tabs. Browser is not available." }
AuthorizationIntentResult.BrowserNotAvailable
}

when (result) {
AuthorizationIntentResult.NotSupported -> {
Expand All @@ -59,6 +67,9 @@ class AccountOAuthViewModel(
}
}

AuthorizationIntentResult.BrowserNotAvailable ->
updateErrorState(Error.BrowserNotAvailable)

is AuthorizationIntentResult.Success -> {
emitEffect(Effect.LaunchOAuth(result.intent))
}
Expand Down
Comment thread
rafaeltonholo marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package app.k9mail.feature.account.oauth.ui

import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Intent
import app.k9mail.core.ui.compose.testing.mvi.runMviTest
import app.k9mail.core.ui.compose.testing.mvi.turbinesWithInitialStateCheck
import app.k9mail.feature.account.common.domain.entity.AuthorizationState
import app.k9mail.feature.account.oauth.domain.AccountOAuthDomainContract.UseCase
import app.k9mail.feature.account.oauth.domain.entity.AuthorizationIntentResult
import app.k9mail.feature.account.oauth.domain.entity.AuthorizationResult
import app.k9mail.feature.account.oauth.ui.AccountOAuthContract.Effect
Expand All @@ -19,6 +21,7 @@ import kotlin.test.Test
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.delay
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import net.thunderbird.core.logging.testing.TestLogger
import net.thunderbird.core.testing.coroutines.MainDispatcherHelper
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
Expand Down Expand Up @@ -159,6 +162,24 @@ class AccountOAuthViewModelTest {
)
}

@Test
fun `should set error state when onOAuthResult received with BrowserNotAvailable`() = runMviTest {
val initialState = defaultState
val testSubject = createTestSubject(
getOAuthRequestIntent = { _, _ -> throw ActivityNotFoundException("browser not available") },
initialState = initialState,
)
val turbines = turbinesWithInitialStateCheck(testSubject, initialState)

testSubject.event(Event.SignInClicked)

assertThat(turbines.stateTurbine.awaitItem()).isEqualTo(
initialState.copy(
error = Error.BrowserNotAvailable,
),
)
}

@Test
fun `should finish OAuth sign in when onOAuthResult received with success but authorization result is cancelled`() =
runMviTest {
Expand Down Expand Up @@ -231,10 +252,20 @@ class AccountOAuthViewModelTest {
authorizationResult: AuthorizationResult = AuthorizationResult.Success(AuthorizationState()),
isGoogleSignIn: Boolean = false,
initialState: State = State(),
) = createTestSubject(
getOAuthRequestIntent = { _, _ -> authorizationIntentResult },
authorizationResult = authorizationResult,
isGoogleSignIn = isGoogleSignIn,
initialState = initialState,
)

fun createTestSubject(
getOAuthRequestIntent: UseCase.GetOAuthRequestIntent,
authorizationResult: AuthorizationResult = AuthorizationResult.Success(AuthorizationState()),
isGoogleSignIn: Boolean = false,
initialState: State = State(),
) = AccountOAuthViewModel(
getOAuthRequestIntent = { _, _ ->
authorizationIntentResult
},
getOAuthRequestIntent = getOAuthRequestIntent,
finishOAuthSignIn = { _ ->
delay(50)
authorizationResult
Expand All @@ -243,6 +274,7 @@ class AccountOAuthViewModelTest {
isGoogleSignIn
},
initialState = initialState,
logger = TestLogger(),
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ fun FeatureLauncherNavHost(

thundermailNavigation.registerRoutes(
navGraphBuilder = this,
onBack = onBack,
onBack = { navController.popBackStack() },
onFinish = { route ->
when (route) {
is ThundermailRoute.IncomingSettings ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ internal class AuthViewModel(
_uiState.update { AuthFlowState.NotSupported }
}

AuthorizationIntentResult.BrowserNotAvailable -> _uiState.update { AuthFlowState.BrowserNotFound }

is AuthorizationIntentResult.Success -> resultObserver.login(authRequestIntentResult.intent)
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package net.thunderbird.feature.thundermail.internal.common.ui

import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import app.k9mail.core.ui.compose.designsystem.PreviewWithThemeLightDark

@Preview
@Composable
private fun ThundermailOAuthRedirectScreenPreview(
@PreviewParameter(ThundermailOAuthRedirectScreenPreviewColProvider::class) param:
Pair<String, ThundermailContract.State>,
) {
val (_, state) = param
PreviewWithThemeLightDark {
ThundermailOAuthRedirectScreen(
state = state,
onBack = {},
modifier = Modifier.fillMaxSize(),
)
}
}

private class ThundermailOAuthRedirectScreenPreviewColProvider :
CollectionPreviewParameterProvider<Pair<String, ThundermailContract.State>>(
listOf(
"Default" to ThundermailContract.State(),
"Browser not available error" to ThundermailContract.State(
error = ThundermailContract.Error.BrowserNotAvailable,
),
"Canceled error" to ThundermailContract.State(
error = ThundermailContract.Error.Canceled,
),
"Unknown error" to ThundermailContract.State(
error = ThundermailContract.Error.Unknown(Exception("Something went wrong")),
),
),
) {
override fun getDisplayName(index: Int): String = values.elementAt(index).first
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ val featureThundermailCommonModule = module {
getOAuthRequestIntent = get(),
finishOAuthSignIn = get(),
checkIsGoogleSignIn = get(),
logger = get(),
)
}
viewModel<ThundermailContract.ViewModel> {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,39 +1,16 @@
package net.thunderbird.feature.thundermail.internal.common.navigation

import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.safeContentPadding
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.navigation.NavGraphBuilder
import app.k9mail.core.ui.compose.designsystem.atom.CircularProgressIndicator
import app.k9mail.core.ui.compose.designsystem.atom.text.TextBodyLarge
import app.k9mail.feature.account.setup.navigation.AccountSetupNavHost
import app.k9mail.feature.account.setup.navigation.AccountSetupRoute
import app.k9mail.feature.onboarding.permissions.ui.PermissionsScreen
import app.k9mail.feature.settings.import.ui.SettingsImportAction
import app.k9mail.feature.settings.import.ui.SettingsImportScreen
import net.thunderbird.core.ui.compose.theme2.MainTheme
import net.thunderbird.core.ui.contract.mvi.observe
import net.thunderbird.core.ui.navigation.deepLinkComposable
import net.thunderbird.feature.thundermail.internal.common.R
import net.thunderbird.feature.thundermail.internal.common.ui.ThundermailContract
import net.thunderbird.feature.thundermail.internal.common.ui.ThundermailOAuthRedirectScreen
import net.thunderbird.feature.thundermail.navigation.ThundermailNavigation
import net.thunderbird.feature.thundermail.navigation.ThundermailRoute
import net.thunderbird.feature.thundermail.navigation.ThundermailRoute.Companion.ACCOUNT_ID_ROUTE_PARAM
import org.koin.androidx.compose.koinViewModel

class DefaultThundermailNavigation : ThundermailNavigation {
override fun registerRoutes(
Expand All @@ -45,7 +22,7 @@ class DefaultThundermailNavigation : ThundermailNavigation {
deepLinkComposable<ThundermailRoute.SignInWithThundermail>(
basePath = ThundermailRoute.SIGN_IN_WITH_THUNDERMAIL_ROUTE,
) {
ThundermailOAuthRedirectScreen(onFinish = onFinish)
ThundermailOAuthRedirectScreen(onFinish = onFinish, onBack = onBack)
}

deepLinkComposable<ThundermailRoute.ScanQrCode>(
Expand Down Expand Up @@ -94,49 +71,3 @@ class DefaultThundermailNavigation : ThundermailNavigation {
}
}
}

@Composable
private fun ThundermailOAuthRedirectScreen(
viewModel: ThundermailContract.ViewModel = koinViewModel<ThundermailContract.ViewModel>(),
onFinish: (ThundermailRoute) -> Unit,
) {
val oAuthLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartActivityForResult(),
) {
viewModel.event(ThundermailContract.Event.OnOAuthResult(it.resultCode, it.data))
}

val (state, dispatch) = viewModel.observe { effect ->
when (effect) {
is ThundermailContract.Effect.LaunchOAuth -> oAuthLauncher.launch(effect.intent)
is ThundermailContract.Effect.NavigateToIncomingServerSettings ->
onFinish(ThundermailRoute.IncomingSettings)
}
}

var launchedOAuth by remember { mutableStateOf(false) }

LaunchedEffect(state.value.initialized) {
if (state.value.initialized && !launchedOAuth) {
dispatch(ThundermailContract.Event.SignInClicked)
launchedOAuth = true
}
}

Box(
modifier = Modifier
.fillMaxSize()
.safeContentPadding(),
) {
Column(
modifier = Modifier.align(Alignment.Center),
verticalArrangement = Arrangement.spacedBy(MainTheme.spacings.double),
horizontalAlignment = Alignment.CenterHorizontally,
) {
CircularProgressIndicator(
modifier = Modifier.size(MainTheme.sizes.medium),
)
TextBodyLarge(stringResource(R.string.thundermail_redirecting))
}
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
package net.thunderbird.feature.thundermail.internal.common.ui

import android.content.Intent
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
import com.fsck.k9.mail.ServerSettings
import net.thunderbird.core.ui.contract.mvi.BaseViewModel

interface ThundermailContract {
@Stable
abstract class ViewModel(initialState: State) : BaseViewModel<State, Event, Effect>(initialState)

@Immutable
data class State(
val initialized: Boolean = false,
val incomingServerSettings: ServerSettings? = null,
Expand All @@ -26,6 +31,7 @@ interface ThundermailContract {
data class OnOAuthResult(val resultCode: Int, val data: Intent?) : Event
}

@Immutable
sealed interface Error {
data object Canceled : Error
data object BrowserNotAvailable : Error
Expand Down
Loading
Loading