Restore OIDC support.

This commit is contained in:
Benoit Marty 2023-08-23 11:55:05 +02:00
parent 92fe22d9d7
commit 06a9b129d0
23 changed files with 164 additions and 87 deletions

View file

@ -17,6 +17,7 @@
package io.element.android.features.login.impl.screens.confirmaccountprovider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
@ -26,8 +27,11 @@ import androidx.compose.runtime.rememberCoroutineScope
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import io.element.android.features.login.api.oidc.OidcAction
import io.element.android.features.login.impl.DefaultLoginUserStory
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
import io.element.android.features.login.impl.error.ChangeServerError
import io.element.android.features.login.impl.oidc.customtab.DefaultOidcActionFlow
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.runCatchingUpdatingState
@ -40,7 +44,9 @@ import java.net.URL
class ConfirmAccountProviderPresenter @AssistedInject constructor(
@Assisted private val params: Params,
private val accountProviderDataSource: AccountProviderDataSource,
private val authenticationService: MatrixAuthenticationService
private val authenticationService: MatrixAuthenticationService,
private val defaultOidcActionFlow: DefaultOidcActionFlow,
private val defaultLoginUserStory: DefaultLoginUserStory,
) : Presenter<ConfirmAccountProviderState> {
data class Params(
@ -61,6 +67,14 @@ class ConfirmAccountProviderPresenter @AssistedInject constructor(
mutableStateOf(Async.Uninitialized)
}
LaunchedEffect(Unit) {
launch {
defaultOidcActionFlow.collect {
onOidcAction(it, loginFlowAction)
}
}
}
fun handleEvents(event: ConfirmAccountProviderEvents) {
when (event) {
ConfirmAccountProviderEvents.Continue -> {
@ -97,4 +111,33 @@ class ConfirmAccountProviderPresenter @AssistedInject constructor(
}.getOrThrow()
}.runCatchingUpdatingState(loginFlowAction, errorTransform = ChangeServerError::from)
}
private suspend fun onOidcAction(
oidcAction: OidcAction?,
loginFlowAction: MutableState<Async<LoginFlow>>,
) {
oidcAction ?: return
loginFlowAction.value = Async.Loading()
when (oidcAction) {
OidcAction.GoBack -> {
authenticationService.cancelOidcLogin()
.onSuccess {
loginFlowAction.value = Async.Uninitialized
}
.onFailure { failure ->
loginFlowAction.value = Async.Failure(failure)
}
}
is OidcAction.Success -> {
authenticationService.loginWithOidc(oidcAction.url)
.onSuccess { _ ->
defaultLoginUserStory.setLoginFlowIsDone(true)
}
.onFailure { failure ->
loginFlowAction.value = Async.Failure(failure)
}
}
}
defaultOidcActionFlow.reset()
}
}

View file

@ -21,7 +21,7 @@ import io.element.android.features.login.impl.accountprovider.AccountProvider
object LoginConstants {
const val MATRIX_ORG_URL = "matrix.org"
const val DEFAULT_HOMESERVER_URL = "matrix.org" // TODO Oidc "synapse-oidc.lab.element.dev"
const val DEFAULT_HOMESERVER_URL = "matrix.org"
const val SLIDING_SYNC_READ_MORE_URL = "https://github.com/matrix-org/sliding-sync/blob/main/docs/Landing.md"
}

View file

@ -20,9 +20,12 @@ import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.features.login.impl.DefaultLoginUserStory
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
import io.element.android.features.login.impl.oidc.customtab.DefaultOidcActionFlow
import io.element.android.features.login.impl.util.defaultAccountProvider
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
import io.element.android.libraries.matrix.test.A_HOMESERVER
import io.element.android.libraries.matrix.test.A_HOMESERVER_OIDC
import io.element.android.libraries.matrix.test.A_THROWABLE
@ -33,11 +36,7 @@ import org.junit.Test
class ConfirmAccountProviderPresenterTest {
@Test
fun `present - initial test`() = runTest {
val presenter = ConfirmAccountProviderPresenter(
ConfirmAccountProviderPresenter.Params(isAccountCreation = false),
AccountProviderDataSource(),
FakeAuthenticationService(),
)
val presenter = createConfirmAccountProviderPresenter()
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
@ -51,13 +50,11 @@ class ConfirmAccountProviderPresenterTest {
@Test
fun `present - continue password login`() = runTest {
val authServer = FakeAuthenticationService()
val presenter = ConfirmAccountProviderPresenter(
ConfirmAccountProviderPresenter.Params(isAccountCreation = false),
AccountProviderDataSource(),
authServer,
val authenticationService = FakeAuthenticationService()
val presenter = createConfirmAccountProviderPresenter(
matrixAuthenticationService = authenticationService,
)
authServer.givenHomeserver(A_HOMESERVER)
authenticationService.givenHomeserver(A_HOMESERVER)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
@ -75,13 +72,11 @@ class ConfirmAccountProviderPresenterTest {
@Test
fun `present - continue oidc`() = runTest {
val authServer = FakeAuthenticationService()
val presenter = ConfirmAccountProviderPresenter(
ConfirmAccountProviderPresenter.Params(isAccountCreation = false),
AccountProviderDataSource(),
authServer,
val authenticationService = FakeAuthenticationService()
val presenter = createConfirmAccountProviderPresenter(
matrixAuthenticationService = authenticationService,
)
authServer.givenHomeserver(A_HOMESERVER_OIDC)
authenticationService.givenHomeserver(A_HOMESERVER_OIDC)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
@ -99,17 +94,15 @@ class ConfirmAccountProviderPresenterTest {
@Test
fun `present - submit fails`() = runTest {
val authServer = FakeAuthenticationService()
val presenter = ConfirmAccountProviderPresenter(
ConfirmAccountProviderPresenter.Params(isAccountCreation = false),
AccountProviderDataSource(),
authServer,
val authenticationService = FakeAuthenticationService()
val presenter = createConfirmAccountProviderPresenter(
matrixAuthenticationService = authenticationService,
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
authServer.givenChangeServerError(Throwable())
authenticationService.givenChangeServerError(Throwable())
initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue)
skipItems(1) // Loading
val failureState = awaitItem()
@ -121,10 +114,8 @@ class ConfirmAccountProviderPresenterTest {
@Test
fun `present - clear error`() = runTest {
val authenticationService = FakeAuthenticationService()
val presenter = ConfirmAccountProviderPresenter(
ConfirmAccountProviderPresenter.Params(isAccountCreation = false),
AccountProviderDataSource(),
authenticationService,
val presenter = createConfirmAccountProviderPresenter(
matrixAuthenticationService = authenticationService,
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
@ -147,4 +138,18 @@ class ConfirmAccountProviderPresenterTest {
assertThat(clearedState.loginFlow).isEqualTo(Async.Uninitialized)
}
}
private fun createConfirmAccountProviderPresenter(
params: ConfirmAccountProviderPresenter.Params = ConfirmAccountProviderPresenter.Params(isAccountCreation = false),
accountProviderDataSource: AccountProviderDataSource = AccountProviderDataSource(),
matrixAuthenticationService: MatrixAuthenticationService = FakeAuthenticationService(),
defaultOidcActionFlow: DefaultOidcActionFlow = DefaultOidcActionFlow(),
defaultLoginUserStory: DefaultLoginUserStory = DefaultLoginUserStory(),
) = ConfirmAccountProviderPresenter(
params = params,
accountProviderDataSource = accountProviderDataSource,
authenticationService = matrixAuthenticationService,
defaultOidcActionFlow = defaultOidcActionFlow,
defaultLoginUserStory = defaultLoginUserStory,
)
}

View file

@ -34,12 +34,12 @@ import io.element.android.libraries.designsystem.preview.ElementPreviewLight
@Composable
fun LogoutPreferenceView(
state: LogoutPreferenceState,
onSuccessLogout: () -> Unit = {}
onSuccessLogout: (String?) -> Unit = {}
) {
val eventSink = state.eventSink
if (state.logoutAction is Async.Success) {
LaunchedEffect(state.logoutAction) {
onSuccessLogout()
onSuccessLogout(state.logoutAction.data)
}
return
}

View file

@ -19,6 +19,6 @@ package io.element.android.features.logout.api
import io.element.android.libraries.architecture.Async
data class LogoutPreferenceState(
val logoutAction: Async<Unit>,
val logoutAction: Async<String?>,
val eventSink: (LogoutPreferenceEvents) -> Unit,
)

View file

@ -40,7 +40,7 @@ class DefaultLogoutPreferencePresenter @Inject constructor(private val matrixCli
@Composable
override fun present(): LogoutPreferenceState {
val localCoroutineScope = rememberCoroutineScope()
val logoutAction: MutableState<Async<Unit>> = remember {
val logoutAction: MutableState<Async<String?>> = remember {
mutableStateOf(Async.Uninitialized)
}
@ -56,7 +56,7 @@ class DefaultLogoutPreferencePresenter @Inject constructor(private val matrixCli
)
}
private fun CoroutineScope.logout(logoutAction: MutableState<Async<Unit>>) = launch {
private fun CoroutineScope.logout(logoutAction: MutableState<Async<String?>>) = launch {
suspend {
matrixClient.logout()
}.runCatchingUpdatingState(logoutAction)

View file

@ -16,8 +16,10 @@
package io.element.android.features.preferences.impl.root
import android.app.Activity
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
@ -25,7 +27,9 @@ import com.bumble.appyx.core.plugin.plugins
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode
import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab
import io.element.android.libraries.di.SessionScope
import timber.log.Timber
@ContributesNode(SessionScope::class)
class PreferencesRootNode @AssistedInject constructor(
@ -65,6 +69,7 @@ class PreferencesRootNode @AssistedInject constructor(
@Composable
override fun View(modifier: Modifier) {
val state = presenter.present()
val activity = LocalContext.current as Activity
PreferencesRootView(
state = state,
modifier = modifier,
@ -73,7 +78,15 @@ class PreferencesRootNode @AssistedInject constructor(
onOpenAnalytics = this::onOpenAnalytics,
onOpenAbout = this::onOpenAbout,
onVerifyClicked = this::onVerifyClicked,
onOpenDeveloperSettings = this::onOpenDeveloperSettings
onOpenDeveloperSettings = this::onOpenDeveloperSettings,
onSuccessLogout = { onSuccessLogout(activity, it) }
)
}
private fun onSuccessLogout(activity: Activity, url: String?) {
Timber.d("Success logout with result url: $url")
url?.let {
activity.openUrlInChromeCustomTab(null, false, it)
}
}
}

View file

@ -55,6 +55,7 @@ fun PreferencesRootView(
onOpenRageShake: () -> Unit,
onOpenAbout: () -> Unit,
onOpenDeveloperSettings: () -> Unit,
onSuccessLogout: (String?) -> Unit,
modifier: Modifier = Modifier,
) {
val snackbarHostState = rememberSnackbarHostState(snackbarMessage = state.snackbarMessage)
@ -98,6 +99,7 @@ fun PreferencesRootView(
HorizontalDivider()
LogoutPreferenceView(
state = state.logoutState,
onSuccessLogout = onSuccessLogout,
)
Text(
modifier = Modifier
@ -140,5 +142,6 @@ private fun ContentToPreview(matrixUser: MatrixUser) {
onOpenDeveloperSettings = {},
onOpenAbout = {},
onVerifyClicked = {},
onSuccessLogout = {},
)
}