Add CheckIsGoogleSignIn and use GoogleSignInButton

This commit is contained in:
Wolf-Martell Montwé 2023-07-20 13:46:57 +02:00
parent 615e16895f
commit 35020a1cb9
No known key found for this signature in database
GPG key ID: 6D45B21512ACBF72
11 changed files with 245 additions and 85 deletions

View file

@ -1,5 +1,6 @@
package com.fsck.k9.activity.setup
@Deprecated("Could be removed with the legacy account setup UI")
object GoogleOAuthHelper {
fun isGoogle(hostname: String): Boolean {
return hostname.lowercase().endsWith(".gmail.com") ||

View file

@ -5,6 +5,7 @@ import app.k9mail.feature.account.oauth.data.AuthorizationRepository
import app.k9mail.feature.account.oauth.data.AuthorizationStateRepository
import app.k9mail.feature.account.oauth.domain.DomainContract
import app.k9mail.feature.account.oauth.domain.DomainContract.UseCase
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
@ -45,10 +46,13 @@ val featureAccountOAuthModule: Module = module {
factory<UseCase.FinishOAuthSignIn> { FinishOAuthSignIn(repository = get()) }
factory<UseCase.CheckIsGoogleSignIn> { CheckIsGoogleSignIn() }
viewModel {
AccountOAuthViewModel(
getOAuthRequestIntent = get(),
finishOAuthSignIn = get(),
checkIsGoogleSignIn = get(),
)
}
}

View file

@ -26,6 +26,10 @@ interface DomainContract {
fun interface CheckIsAuthorized {
suspend fun execute(authorizationState: AuthorizationState): Boolean
}
fun interface CheckIsGoogleSignIn {
fun execute(hostname: String): Boolean
}
}
interface AuthorizationRepository {

View file

@ -0,0 +1,23 @@
package app.k9mail.feature.account.oauth.domain.usecase
import app.k9mail.feature.account.oauth.domain.DomainContract.UseCase
internal class CheckIsGoogleSignIn : UseCase.CheckIsGoogleSignIn {
override fun execute(hostname: String): Boolean {
for (domain in domainList) {
if (hostname.lowercase().endsWith(domain)) {
return true
}
}
return false
}
private companion object {
val domainList = listOf(
".gmail.com",
".googlemail.com",
".google.com",
)
}
}

View file

@ -55,6 +55,7 @@ internal fun AccountOAuthContent(
SignInItem(
emailAddress = state.emailAddress,
onSignInClick = { onEvent(Event.SignInClicked) },
isGoogleSignIn = state.isGoogleSignIn,
)
}
}

View file

@ -10,7 +10,6 @@ import app.k9mail.core.ui.compose.common.mvi.observe
import app.k9mail.core.ui.compose.designsystem.template.Scaffold
import app.k9mail.feature.account.common.ui.AppTitleTopHeader
import app.k9mail.feature.account.common.ui.WizardNavigationBar
import app.k9mail.feature.account.common.ui.WizardNavigationBarState
import app.k9mail.feature.account.oauth.R
import app.k9mail.feature.account.oauth.domain.entity.OAuthResult
import app.k9mail.feature.account.oauth.ui.AccountOAuthContract.Effect

View file

@ -18,11 +18,16 @@ class AccountOAuthViewModel(
initialState: State = State(),
private val getOAuthRequestIntent: UseCase.GetOAuthRequestIntent,
private val finishOAuthSignIn: UseCase.FinishOAuthSignIn,
private val checkIsGoogleSignIn: UseCase.CheckIsGoogleSignIn,
) : BaseViewModel<State, Event, Effect>(initialState), ViewModel {
override fun initState(state: State) {
val isGoogleSignIn = checkIsGoogleSignIn.execute(state.hostname)
updateState {
state.copy()
state.copy(
isGoogleSignIn = isGoogleSignIn,
)
}
}

View file

@ -10,6 +10,7 @@ import app.k9mail.feature.account.oauth.ui.view.SignInView
internal fun LazyItemScope.SignInItem(
emailAddress: String,
onSignInClick: () -> Unit,
isGoogleSignIn: Boolean,
modifier: Modifier = Modifier,
) {
ListItem(
@ -18,6 +19,7 @@ internal fun LazyItemScope.SignInItem(
SignInView(
emailAddress = emailAddress,
onSignInClick = onSignInClick,
isGoogleSignIn = isGoogleSignIn,
)
}
}

View file

@ -7,6 +7,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import app.k9mail.core.ui.compose.common.DevicePreviews
import app.k9mail.core.ui.compose.designsystem.atom.button.Button
import app.k9mail.core.ui.compose.designsystem.atom.text.TextCaption
import app.k9mail.core.ui.compose.designsystem.atom.text.TextSubtitle1
@ -18,6 +19,7 @@ import app.k9mail.feature.account.oauth.R
internal fun SignInView(
emailAddress: String,
onSignInClick: () -> Unit,
isGoogleSignIn: Boolean,
modifier: Modifier = Modifier,
) {
Column(
@ -38,9 +40,35 @@ internal fun SignInView(
textAlign = TextAlign.Center,
)
Button(
text = stringResource(id = R.string.account_oauth_sign_in_button),
onClick = onSignInClick,
)
if (isGoogleSignIn) {
SignInWithGoogleButton(
onClick = onSignInClick,
)
} else {
Button(
text = stringResource(id = R.string.account_oauth_sign_in_button),
onClick = onSignInClick,
)
}
}
}
@DevicePreviews
@Composable
internal fun SignInViewPreview() {
SignInView(
emailAddress = "test@example.com",
onSignInClick = {},
isGoogleSignIn = false,
)
}
@DevicePreviews
@Composable
internal fun SignInViewWithGooglePreview() {
SignInView(
emailAddress = "test@gmail.com",
onSignInClick = {},
isGoogleSignIn = true,
)
}

View file

@ -0,0 +1,37 @@
package app.k9mail.feature.account.oauth.domain.usecase
import assertk.assertThat
import assertk.assertions.isFalse
import assertk.assertions.isTrue
import org.junit.Test
class CheckIsGoogleSignInTest {
private val testSubject = CheckIsGoogleSignIn()
@Test
fun `should return true when hostname ends with a google domain`() {
val hostnames = listOf(
"mail.gmail.com",
"mail.googlemail.com",
"mail.google.com",
)
for (hostname in hostnames) {
assertThat(testSubject.execute(hostname)).isTrue()
}
}
@Test
fun `should return false when hostname does not end with a google domain`() {
val hostnames = listOf(
"mail.example.com",
"mail.example.org",
"mail.example.net",
)
for (hostname in hostnames) {
assertThat(testSubject.execute(hostname)).isFalse()
}
}
}

View file

@ -27,6 +27,60 @@ class AccountOAuthViewModelTest {
@get:Rule
val mainDispatcherRule = MainDispatcherRule()
@Test
fun `should change state when google hostname found on initState`() = runTest {
val testSubject = createTestSubject(
isGoogleSignIn = true,
)
val stateTurbine = testSubject.state.testIn(backgroundScope)
val effectTurbine = testSubject.effect.testIn(backgroundScope)
val turbines = listOf(stateTurbine, effectTurbine)
assertThatAndTurbinesConsumed(
actual = stateTurbine.awaitItem(),
turbines = turbines,
) {
isEqualTo(State())
}
testSubject.initState(defaultState)
assertThatAndTurbinesConsumed(
actual = stateTurbine.awaitItem(),
turbines = turbines,
) {
isEqualTo(defaultState.copy(isGoogleSignIn = true))
}
}
@Test
fun `should not change state when no google hostname found on initState`() = runTest {
val testSubject = createTestSubject(
isGoogleSignIn = false,
)
val stateTurbine = testSubject.state.testIn(backgroundScope)
val effectTurbine = testSubject.effect.testIn(backgroundScope)
val turbines = listOf(stateTurbine, effectTurbine)
assertThatAndTurbinesConsumed(
actual = stateTurbine.awaitItem(),
turbines = turbines,
) {
isEqualTo(State())
}
testSubject.initState(defaultState)
assertThatAndTurbinesConsumed(
actual = stateTurbine.awaitItem(),
turbines = turbines,
) {
isEqualTo(defaultState.copy(isGoogleSignIn = false))
}
}
@Test
fun `should launch OAuth when SignInClicked event received`() = runTest {
val initialState = defaultState
@ -118,7 +172,7 @@ class AccountOAuthViewModelTest {
val authorizationState = AuthorizationState(state = "state")
val testSubject = createTestSubject(
authorizationResult = AuthorizationResult.Success(authorizationState),
initialState = initialState
initialState = initialState,
)
val stateTurbine = testSubject.state.testIn(backgroundScope)
val effectTurbine = testSubject.effect.testIn(backgroundScope)
@ -162,7 +216,7 @@ class AccountOAuthViewModelTest {
val initialState = defaultState
val testSubject = createTestSubject(
authorizationResult = AuthorizationResult.Canceled,
initialState = initialState
initialState = initialState,
)
val stateTurbine = testSubject.state.testIn(backgroundScope)
val effectTurbine = testSubject.effect.testIn(backgroundScope)
@ -190,90 +244,92 @@ class AccountOAuthViewModelTest {
}
@Test
fun `should finish OAuth sign in when onOAuthResult received with success but authorization result is cancelled`() = runTest {
val initialState = defaultState
val testSubject = createTestSubject(
authorizationResult = AuthorizationResult.Canceled,
initialState = initialState
)
val stateTurbine = testSubject.state.testIn(backgroundScope)
val effectTurbine = testSubject.effect.testIn(backgroundScope)
val turbines = listOf(stateTurbine, effectTurbine)
fun `should finish OAuth sign in when onOAuthResult received with success but authorization result is cancelled`() =
runTest {
val initialState = defaultState
val testSubject = createTestSubject(
authorizationResult = AuthorizationResult.Canceled,
initialState = initialState,
)
val stateTurbine = testSubject.state.testIn(backgroundScope)
val effectTurbine = testSubject.effect.testIn(backgroundScope)
val turbines = listOf(stateTurbine, effectTurbine)
assertThatAndTurbinesConsumed(
actual = stateTurbine.awaitItem(),
turbines = turbines,
) {
isEqualTo(initialState)
assertThatAndTurbinesConsumed(
actual = stateTurbine.awaitItem(),
turbines = turbines,
) {
isEqualTo(initialState)
}
testSubject.event(Event.OnOAuthResult(resultCode = Activity.RESULT_OK, data = intent))
val loadingState = initialState.copy(isLoading = true)
assertThatAndTurbinesConsumed(
actual = stateTurbine.awaitItem(),
turbines = turbines,
) {
isEqualTo(loadingState)
}
val failureState = loadingState.copy(
isLoading = false,
error = Error.Cancelled,
)
assertThatAndTurbinesConsumed(
actual = stateTurbine.awaitItem(),
turbines = turbines,
) {
isEqualTo(failureState)
}
}
testSubject.event(Event.OnOAuthResult(resultCode = Activity.RESULT_OK, data = intent))
val loadingState = initialState.copy(isLoading = true)
assertThatAndTurbinesConsumed(
actual = stateTurbine.awaitItem(),
turbines = turbines,
) {
isEqualTo(loadingState)
}
val failureState = loadingState.copy(
isLoading = false,
error = Error.Cancelled,
)
assertThatAndTurbinesConsumed(
actual = stateTurbine.awaitItem(),
turbines = turbines,
) {
isEqualTo(failureState)
}
}
@Test
fun `should finish OAuth sign in when onOAuthResult received with success but authorization result is failure`() = runTest {
val initialState = defaultState
val failure = Exception("failure")
val testSubject = createTestSubject(
authorizationResult = AuthorizationResult.Failure(failure),
initialState = initialState
)
val stateTurbine = testSubject.state.testIn(backgroundScope)
val effectTurbine = testSubject.effect.testIn(backgroundScope)
val turbines = listOf(stateTurbine, effectTurbine)
fun `should finish OAuth sign in when onOAuthResult received with success but authorization result is failure`() =
runTest {
val initialState = defaultState
val failure = Exception("failure")
val testSubject = createTestSubject(
authorizationResult = AuthorizationResult.Failure(failure),
initialState = initialState,
)
val stateTurbine = testSubject.state.testIn(backgroundScope)
val effectTurbine = testSubject.effect.testIn(backgroundScope)
val turbines = listOf(stateTurbine, effectTurbine)
assertThatAndTurbinesConsumed(
actual = stateTurbine.awaitItem(),
turbines = turbines,
) {
isEqualTo(initialState)
assertThatAndTurbinesConsumed(
actual = stateTurbine.awaitItem(),
turbines = turbines,
) {
isEqualTo(initialState)
}
testSubject.event(Event.OnOAuthResult(resultCode = Activity.RESULT_OK, data = intent))
val loadingState = initialState.copy(isLoading = true)
assertThatAndTurbinesConsumed(
actual = stateTurbine.awaitItem(),
turbines = turbines,
) {
isEqualTo(loadingState)
}
val failureState = loadingState.copy(
isLoading = false,
error = Error.Unknown(failure),
)
assertThatAndTurbinesConsumed(
actual = stateTurbine.awaitItem(),
turbines = turbines,
) {
isEqualTo(failureState)
}
}
testSubject.event(Event.OnOAuthResult(resultCode = Activity.RESULT_OK, data = intent))
val loadingState = initialState.copy(isLoading = true)
assertThatAndTurbinesConsumed(
actual = stateTurbine.awaitItem(),
turbines = turbines,
) {
isEqualTo(loadingState)
}
val failureState = loadingState.copy(
isLoading = false,
error = Error.Unknown(failure),
)
assertThatAndTurbinesConsumed(
actual = stateTurbine.awaitItem(),
turbines = turbines,
) {
isEqualTo(failureState)
}
}
@Test
fun `should emit NavigateBack effect when OnBackClicked event received`() = runTest {
val viewModel = createTestSubject()