Add OIDC support
This commit is contained in:
parent
ff1147e611
commit
f1d2f566bc
26 changed files with 789 additions and 44 deletions
|
|
@ -29,11 +29,13 @@ import dagger.assisted.Assisted
|
|||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.anvilannotations.ContributesNode
|
||||
import io.element.android.features.login.impl.changeserver.ChangeServerNode
|
||||
import io.element.android.features.login.impl.oidc.OidcNode
|
||||
import io.element.android.features.login.impl.root.LoginRootNode
|
||||
import io.element.android.libraries.architecture.BackstackNode
|
||||
import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler
|
||||
import io.element.android.libraries.architecture.createNode
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.matrix.api.auth.OidcDetails
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@ContributesNode(AppScope::class)
|
||||
|
|
@ -55,6 +57,9 @@ class LoginFlowNode @AssistedInject constructor(
|
|||
|
||||
@Parcelize
|
||||
object ChangeServer : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data class OidcView(val oidcDetails: OidcDetails) : NavTarget
|
||||
}
|
||||
|
||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||
|
|
@ -64,10 +69,19 @@ class LoginFlowNode @AssistedInject constructor(
|
|||
override fun onChangeHomeServer() {
|
||||
backstack.push(NavTarget.ChangeServer)
|
||||
}
|
||||
|
||||
override fun onOidcDetails(oidcDetails: OidcDetails) {
|
||||
backstack.push(NavTarget.OidcView(oidcDetails))
|
||||
}
|
||||
}
|
||||
createNode<LoginRootNode>(buildContext, plugins = listOf(callback))
|
||||
}
|
||||
|
||||
NavTarget.ChangeServer -> createNode<ChangeServerNode>(buildContext)
|
||||
is NavTarget.OidcView -> {
|
||||
val input = OidcNode.Inputs(navTarget.oidcDetails)
|
||||
createNode<OidcNode>(buildContext, plugins = listOf(input))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.login.impl.oidc
|
||||
|
||||
sealed interface OidcEvents {
|
||||
object Cancel : OidcEvents
|
||||
data class OidcActionEvent(val oidcAction: OidcAction): OidcEvents
|
||||
object ClearError : OidcEvents
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.login.impl.oidc
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.anvilannotations.ContributesNode
|
||||
import io.element.android.libraries.architecture.NodeInputs
|
||||
import io.element.android.libraries.architecture.inputs
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.matrix.api.auth.OidcDetails
|
||||
|
||||
/**
|
||||
* TODO Transmit back press to the webview
|
||||
*/
|
||||
@ContributesNode(AppScope::class)
|
||||
class OidcNode @AssistedInject constructor(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
presenterFactory: OidcPresenter.Factory,
|
||||
) : Node(buildContext, plugins = plugins) {
|
||||
|
||||
data class Inputs(
|
||||
val oidcDetails: OidcDetails,
|
||||
) : NodeInputs
|
||||
|
||||
private val inputs: Inputs = inputs()
|
||||
private val presenter = presenterFactory.create(inputs.oidcDetails)
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
val state = presenter.present()
|
||||
OidcView(
|
||||
state = state,
|
||||
modifier = modifier,
|
||||
onNavigateBack = ::navigateUp,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.login.impl.oidc
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
|
||||
import io.element.android.libraries.matrix.api.auth.OidcDetails
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class OidcPresenter @AssistedInject constructor(
|
||||
@Assisted private val oidcDetails: OidcDetails,
|
||||
private val authenticationService: MatrixAuthenticationService,
|
||||
) : Presenter<OidcState> {
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(oidcDetails: OidcDetails): OidcPresenter
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun present(): OidcState {
|
||||
var requestState: Async<Unit> by remember {
|
||||
mutableStateOf(Async.Uninitialized)
|
||||
}
|
||||
val localCoroutineScope = rememberCoroutineScope()
|
||||
|
||||
fun handleCancel() {
|
||||
requestState = Async.Loading()
|
||||
localCoroutineScope.launch {
|
||||
requestState = try {
|
||||
authenticationService.cancelOidcLogin()
|
||||
// Then go back
|
||||
Async.Success(Unit)
|
||||
} catch (throwable: Throwable) {
|
||||
Async.Failure(throwable)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun handleSuccess(url: String) {
|
||||
requestState = Async.Loading()
|
||||
localCoroutineScope.launch {
|
||||
try {
|
||||
authenticationService.loginWithOidc(url)
|
||||
// Then the node tree will be updated, there is nothing to do
|
||||
} catch (throwable: Throwable) {
|
||||
requestState = Async.Failure(throwable)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun handleAction(action: OidcAction) {
|
||||
when (action) {
|
||||
OidcAction.GoBack -> handleCancel()
|
||||
is OidcAction.Success -> handleSuccess(action.url)
|
||||
}
|
||||
}
|
||||
|
||||
fun handleEvents(event: OidcEvents) {
|
||||
when (event) {
|
||||
OidcEvents.Cancel -> handleCancel()
|
||||
is OidcEvents.OidcActionEvent -> handleAction(event.oidcAction)
|
||||
OidcEvents.ClearError -> requestState = Async.Uninitialized
|
||||
}
|
||||
}
|
||||
|
||||
return OidcState(
|
||||
oidcDetails = oidcDetails,
|
||||
requestState = requestState,
|
||||
eventSink = ::handleEvents
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.login.impl.oidc
|
||||
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.matrix.api.auth.OidcDetails
|
||||
|
||||
data class OidcState(
|
||||
val oidcDetails: OidcDetails,
|
||||
val requestState: Async<Unit>,
|
||||
val eventSink: (OidcEvents) -> Unit
|
||||
)
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.login.impl.oidc
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.matrix.api.auth.OidcDetails
|
||||
|
||||
open class OidcStateProvider : PreviewParameterProvider<OidcState> {
|
||||
override val values: Sequence<OidcState>
|
||||
get() = sequenceOf(
|
||||
aOidcState(),
|
||||
aOidcState().copy(requestState = Async.Loading()),
|
||||
)
|
||||
}
|
||||
|
||||
fun aOidcState() = OidcState(
|
||||
oidcDetails = aOidcDetails(),
|
||||
requestState = Async.Uninitialized,
|
||||
eventSink = {}
|
||||
)
|
||||
|
||||
fun aOidcDetails() = OidcDetails(
|
||||
url = "aUrl",
|
||||
)
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.login.impl.oidc
|
||||
|
||||
import io.element.android.libraries.matrix.api.auth.OidcConfig
|
||||
|
||||
/**
|
||||
* Simple parser for oidc url interception.
|
||||
* TODO Find documentation about the format.
|
||||
*/
|
||||
class OidcUrlParser {
|
||||
|
||||
// When user press button "Cancel", we get the url:
|
||||
// `io.element:/callback?error=access_denied&state=IFF1UETGye2ZA8pO`
|
||||
// On success, we get:
|
||||
// `io.element:/callback?state=IFF1UETGye2ZA8pO&code=y6X1GZeqA3xxOWcTeShgv8nkgFJXyzWB`
|
||||
/**
|
||||
* Return a OidcAction, or null if the url is not a OidcUrl
|
||||
*/
|
||||
fun parse(url: String): OidcAction? {
|
||||
if (!url.startsWith(OidcConfig.redirectUri)) return null
|
||||
if (url.contains("error=access_denied")) return OidcAction.GoBack
|
||||
if (url.contains("code=")) return OidcAction.Success(url)
|
||||
|
||||
// Other case not supported, let's crash the app for now
|
||||
error("Not supported: $url")
|
||||
}
|
||||
}
|
||||
|
||||
sealed interface OidcAction {
|
||||
object GoBack : OidcAction
|
||||
data class Success(val url: String) : OidcAction
|
||||
}
|
||||
|
|
@ -0,0 +1,121 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.login.impl.oidc
|
||||
|
||||
import android.webkit.WebView
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.statusBarsPadding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.core.bool.orFalse
|
||||
import io.element.android.libraries.core.bool.orTrue
|
||||
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
|
||||
|
||||
@Composable
|
||||
fun OidcView(
|
||||
state: OidcState,
|
||||
onNavigateBack: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val oidcUrlParser = remember { OidcUrlParser() }
|
||||
var webView: WebView? = null
|
||||
fun shouldOverrideUrl(url: String): Boolean {
|
||||
val action = oidcUrlParser.parse(url)
|
||||
if (action != null) {
|
||||
state.eventSink.invoke(OidcEvents.OidcActionEvent(action))
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
val oidcWebViewClient = remember {
|
||||
OidcWebViewClient(eventListener = object : WebViewEventListener {
|
||||
override fun shouldOverrideUrlLoading(url: String): Boolean {
|
||||
return shouldOverrideUrl(url)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
BackHandler {
|
||||
if (webView?.canGoBack().orFalse()) {
|
||||
webView?.goBack()
|
||||
} else {
|
||||
// To properly cancel Oidc login
|
||||
state.eventSink.invoke(OidcEvents.Cancel)
|
||||
}
|
||||
}
|
||||
|
||||
Box(modifier = modifier.statusBarsPadding()) {
|
||||
AndroidView(
|
||||
modifier = modifier
|
||||
.statusBarsPadding(),
|
||||
factory = { context ->
|
||||
WebView(context).apply {
|
||||
webViewClient = oidcWebViewClient
|
||||
loadUrl(state.oidcDetails.url)
|
||||
}.also {
|
||||
webView = it
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
when (state.requestState) {
|
||||
Async.Uninitialized -> Unit
|
||||
is Async.Failure -> {
|
||||
ErrorDialog(
|
||||
content = state.requestState.error.toString(),
|
||||
onDismiss = { state.eventSink(OidcEvents.ClearError) }
|
||||
)
|
||||
}
|
||||
is Async.Loading -> {
|
||||
// Indeterminate indicator, to avoid the freeze effect if the connection takes time to initialize.
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.align(Alignment.Center)
|
||||
)
|
||||
}
|
||||
is Async.Success -> onNavigateBack()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun OidcViewLightPreview(@PreviewParameter(OidcStateProvider::class) state: OidcState) =
|
||||
ElementPreviewLight { ContentToPreview(state) }
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun OidcViewDarkPreview(@PreviewParameter(OidcStateProvider::class) state: OidcState) =
|
||||
ElementPreviewDark { ContentToPreview(state) }
|
||||
|
||||
@Composable
|
||||
private fun ContentToPreview(state: OidcState) {
|
||||
OidcView(
|
||||
state = state,
|
||||
onNavigateBack = { },
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.login.impl.oidc
|
||||
|
||||
import android.annotation.TargetApi
|
||||
import android.os.Build
|
||||
import android.webkit.WebResourceRequest
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import timber.log.Timber
|
||||
|
||||
// TODO Move to a dedicated module
|
||||
class OidcWebViewClient(private val eventListener: WebViewEventListener) : WebViewClient() {
|
||||
@TargetApi(Build.VERSION_CODES.N)
|
||||
override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean {
|
||||
return shouldOverrideUrl(request.url.toString())
|
||||
}
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean {
|
||||
return shouldOverrideUrl(url)
|
||||
}
|
||||
|
||||
private fun shouldOverrideUrl(url: String): Boolean {
|
||||
Timber.d("shouldOverrideUrl: $url")
|
||||
return eventListener.shouldOverrideUrlLoading(url)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.login.impl.oidc
|
||||
|
||||
interface WebViewEventListener {
|
||||
/**
|
||||
* Triggered when a Webview loads an url.
|
||||
*
|
||||
* @param url The url about to be rendered.
|
||||
* @return true if the method needs to manage some custom handling
|
||||
*/
|
||||
fun shouldOverrideUrlLoading(url: String): Boolean {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
|
@ -26,6 +26,7 @@ import dagger.assisted.Assisted
|
|||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.anvilannotations.ContributesNode
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.matrix.api.auth.OidcDetails
|
||||
|
||||
@ContributesNode(AppScope::class)
|
||||
class LoginRootNode @AssistedInject constructor(
|
||||
|
|
@ -36,20 +37,26 @@ class LoginRootNode @AssistedInject constructor(
|
|||
|
||||
interface Callback : Plugin {
|
||||
fun onChangeHomeServer()
|
||||
fun onOidcDetails(oidcDetails: OidcDetails)
|
||||
}
|
||||
|
||||
private fun onChangeHomeServer() {
|
||||
plugins<Callback>().forEach { it.onChangeHomeServer() }
|
||||
}
|
||||
|
||||
private fun onOidcDetails(oidcDetails: OidcDetails) {
|
||||
plugins<Callback>().forEach { it.onOidcDetails(oidcDetails) }
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
val state = presenter.present()
|
||||
LoginRootView(
|
||||
state = state,
|
||||
modifier = modifier,
|
||||
onChangeServer = this::onChangeHomeServer,
|
||||
onBackPressed = this::navigateUp
|
||||
onChangeServer = ::onChangeHomeServer,
|
||||
onOidcDetails = ::onOidcDetails,
|
||||
onBackPressed = ::navigateUp
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,7 +33,11 @@ import javax.inject.Inject
|
|||
|
||||
class LoginRootPresenter @Inject constructor(private val authenticationService: MatrixAuthenticationService) : Presenter<LoginRootState> {
|
||||
|
||||
private val defaultHomeserver = MatrixHomeServerDetails(LoginConstants.DEFAULT_HOMESERVER_URL, true, null)
|
||||
private val defaultHomeserver = MatrixHomeServerDetails(
|
||||
url = LoginConstants.DEFAULT_HOMESERVER_URL,
|
||||
supportsPasswordLogin = true,
|
||||
supportsOidc = false,
|
||||
)
|
||||
|
||||
@Composable
|
||||
override fun present(): LoginRootState {
|
||||
|
|
@ -54,7 +58,12 @@ class LoginRootPresenter @Inject constructor(private val authenticationService:
|
|||
is LoginRootEvents.SetPassword -> updateFormState(formState) {
|
||||
copy(password = event.password)
|
||||
}
|
||||
LoginRootEvents.Submit -> localCoroutineScope.submit(homeserver.url, formState.value, loggedInState)
|
||||
LoginRootEvents.Submit -> {
|
||||
when {
|
||||
homeserver.supportsOidc -> localCoroutineScope.submitOidc(homeserver.url, loggedInState)
|
||||
homeserver.supportsPasswordLogin -> localCoroutineScope.submit(homeserver.url, formState.value, loggedInState)
|
||||
}
|
||||
}
|
||||
LoginRootEvents.ClearError -> loggedInState.value = LoggedInState.NotLoggedIn
|
||||
}
|
||||
}
|
||||
|
|
@ -67,9 +76,22 @@ class LoginRootPresenter @Inject constructor(private val authenticationService:
|
|||
)
|
||||
}
|
||||
|
||||
private fun CoroutineScope.submitOidc(homeserver: String, loggedInState: MutableState<LoggedInState>) = launch {
|
||||
loggedInState.value = LoggedInState.LoggingIn
|
||||
// TODO rework the setHomeserver flow
|
||||
authenticationService.setHomeserver(homeserver)
|
||||
authenticationService.getOidcUrl()
|
||||
.onSuccess {
|
||||
loggedInState.value = LoggedInState.OidcStarted(it)
|
||||
}
|
||||
.onFailure { failure ->
|
||||
loggedInState.value = LoggedInState.ErrorLoggingIn(failure)
|
||||
}
|
||||
}
|
||||
|
||||
private fun CoroutineScope.submit(homeserver: String, formState: LoginFormState, loggedInState: MutableState<LoggedInState>) = launch {
|
||||
loggedInState.value = LoggedInState.LoggingIn
|
||||
//TODO rework the setHomeserver flow
|
||||
// TODO rework the setHomeserver flow
|
||||
authenticationService.setHomeserver(homeserver)
|
||||
authenticationService.login(formState.login.trim(), formState.password)
|
||||
.onSuccess { sessionId ->
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ package io.element.android.features.login.impl.root
|
|||
|
||||
import android.os.Parcelable
|
||||
import io.element.android.libraries.matrix.api.auth.MatrixHomeServerDetails
|
||||
import io.element.android.libraries.matrix.api.auth.OidcDetails
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
|
|
@ -27,13 +28,17 @@ data class LoginRootState(
|
|||
val formState: LoginFormState,
|
||||
val eventSink: (LoginRootEvents) -> Unit
|
||||
) {
|
||||
val submitEnabled: Boolean get() =
|
||||
formState.login.isNotEmpty() && formState.password.isNotEmpty() && loggedInState !is LoggedInState.ErrorLoggingIn
|
||||
val supportPasswordLogin = homeserverDetails.supportsPasswordLogin
|
||||
val supportOidcLogin = homeserverDetails.supportsOidc
|
||||
val submitEnabled: Boolean
|
||||
get() = loggedInState !is LoggedInState.ErrorLoggingIn &&
|
||||
((formState.login.isNotEmpty() && formState.password.isNotEmpty()) || supportOidcLogin)
|
||||
}
|
||||
|
||||
sealed interface LoggedInState {
|
||||
object NotLoggedIn : LoggedInState
|
||||
object LoggingIn : LoggedInState
|
||||
data class OidcStarted(val oidcDetail: OidcDetails) : LoggedInState
|
||||
data class ErrorLoggingIn(val failure: Throwable) : LoggedInState
|
||||
data class LoggedIn(val sessionId: SessionId) : LoggedInState
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,16 +24,20 @@ open class LoginRootStateProvider : PreviewParameterProvider<LoginRootState> {
|
|||
override val values: Sequence<LoginRootState>
|
||||
get() = sequenceOf(
|
||||
aLoginRootState(),
|
||||
aLoginRootState().copy(homeserverDetails = MatrixHomeServerDetails("some-custom-server.com", true, null)),
|
||||
aLoginRootState().copy(homeserverDetails = MatrixHomeServerDetails("some-custom-server.com", supportsPasswordLogin = true, supportsOidc = false)),
|
||||
aLoginRootState().copy(formState = LoginFormState("user", "pass")),
|
||||
aLoginRootState().copy(formState = LoginFormState("user", "pass"), loggedInState = LoggedInState.LoggingIn),
|
||||
aLoginRootState().copy(formState = LoginFormState("user", "pass"), loggedInState = LoggedInState.ErrorLoggingIn(Throwable())),
|
||||
aLoginRootState().copy(formState = LoginFormState("user", "pass"), loggedInState = LoggedInState.LoggedIn(SessionId("@user:domain"))),
|
||||
// Oidc
|
||||
aLoginRootState().copy(homeserverDetails = MatrixHomeServerDetails("server-with-oidc.org", supportsPasswordLogin = false, supportsOidc = true)),
|
||||
// No password, no oidc support
|
||||
aLoginRootState().copy(homeserverDetails = MatrixHomeServerDetails("wrong.org", supportsPasswordLogin = false, supportsOidc = false)),
|
||||
)
|
||||
}
|
||||
|
||||
fun aLoginRootState() = LoginRootState(
|
||||
homeserverDetails = MatrixHomeServerDetails("matrix.org", true, null),
|
||||
homeserverDetails = MatrixHomeServerDetails("matrix.org", supportsPasswordLogin = true, supportsOidc = false),
|
||||
loggedInState = LoggedInState.NotLoggedIn,
|
||||
formState = LoginFormState.Default,
|
||||
eventSink = {}
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ import io.element.android.libraries.designsystem.theme.components.TextField
|
|||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||
import io.element.android.libraries.designsystem.theme.components.autofill
|
||||
import io.element.android.libraries.designsystem.theme.components.onTabOrEnterKeyFocusNext
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.api.auth.OidcDetails
|
||||
import io.element.android.libraries.testtags.TestTags
|
||||
import io.element.android.libraries.testtags.testTag
|
||||
import io.element.android.libraries.ui.strings.R as StringR
|
||||
|
|
@ -94,7 +94,7 @@ fun LoginRootView(
|
|||
state: LoginRootState,
|
||||
modifier: Modifier = Modifier,
|
||||
onChangeServer: () -> Unit = {},
|
||||
onLoginWithSuccess: (SessionId) -> Unit = {},
|
||||
onOidcDetails: (OidcDetails) -> Unit = {},
|
||||
onBackPressed: () -> Unit,
|
||||
) {
|
||||
val isLoading by remember(state.loggedInState) {
|
||||
|
|
@ -102,6 +102,15 @@ fun LoginRootView(
|
|||
state.loggedInState == LoggedInState.LoggingIn
|
||||
}
|
||||
}
|
||||
val focusManager = LocalFocusManager.current
|
||||
|
||||
fun submit() {
|
||||
// Clear focus to prevent keyboard issues with textfields
|
||||
focusManager.clearFocus(force = true)
|
||||
|
||||
state.eventSink(LoginRootEvents.Submit)
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
|
|
@ -143,13 +152,37 @@ fun LoginRootView(
|
|||
|
||||
Spacer(Modifier.height(32.dp))
|
||||
|
||||
LoginForm(state = state, isLoading = isLoading)
|
||||
when {
|
||||
state.supportOidcLogin -> {
|
||||
// Oidc, in this case, just display a Spacer and the submit button
|
||||
Spacer(Modifier.height(28.dp))
|
||||
}
|
||||
state.supportPasswordLogin -> {
|
||||
LoginForm(state = state, isLoading = isLoading, onSubmit = ::submit)
|
||||
}
|
||||
else -> {
|
||||
Text(text = "No supported login flow")
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
Spacer(Modifier.height(28.dp))
|
||||
|
||||
if (state.supportOidcLogin || state.supportPasswordLogin) {
|
||||
// Submit
|
||||
ButtonWithProgress(
|
||||
text = stringResource(R.string.screen_login_submit),
|
||||
showProgress = isLoading,
|
||||
onClick = ::submit,
|
||||
enabled = state.submitEnabled,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.testTag(TestTags.loginContinue)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
}
|
||||
}
|
||||
when (val loggedInState = state.loggedInState) {
|
||||
is LoggedInState.LoggedIn -> onLoginWithSuccess(loggedInState.sessionId)
|
||||
is LoggedInState.OidcStarted -> onOidcDetails(loggedInState.oidcDetail)
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
|
|
@ -217,6 +250,7 @@ internal fun ChangeServerSection(
|
|||
internal fun LoginForm(
|
||||
state: LoginRootState,
|
||||
isLoading: Boolean,
|
||||
onSubmit: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
var loginFieldState by textFieldState(stateValue = state.formState.login)
|
||||
|
|
@ -225,13 +259,6 @@ internal fun LoginForm(
|
|||
val focusManager = LocalFocusManager.current
|
||||
val eventSink = state.eventSink
|
||||
|
||||
fun submit() {
|
||||
// Clear focus to prevent keyboard issues with textfields
|
||||
focusManager.clearFocus(force = true)
|
||||
|
||||
eventSink(LoginRootEvents.Submit)
|
||||
}
|
||||
|
||||
Column(modifier) {
|
||||
Text(
|
||||
text = stringResource(R.string.screen_login_form_header),
|
||||
|
|
@ -318,23 +345,11 @@ internal fun LoginForm(
|
|||
imeAction = ImeAction.Done,
|
||||
),
|
||||
keyboardActions = KeyboardActions(
|
||||
onDone = { submit() }
|
||||
onDone = { onSubmit() }
|
||||
),
|
||||
singleLine = true,
|
||||
maxLines = 1,
|
||||
)
|
||||
Spacer(Modifier.height(28.dp))
|
||||
|
||||
// Submit
|
||||
ButtonWithProgress(
|
||||
text = stringResource(R.string.screen_login_submit),
|
||||
showProgress = isLoading,
|
||||
onClick = ::submit,
|
||||
enabled = state.submitEnabled,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.testTag(TestTags.loginContinue)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ package io.element.android.features.login.impl.util
|
|||
|
||||
object LoginConstants {
|
||||
|
||||
const val DEFAULT_HOMESERVER_URL = "matrix.org"
|
||||
const val DEFAULT_HOMESERVER_URL = "synapse-oidc.lab.element.dev" // "matrix.org"
|
||||
const val SLIDING_SYNC_READ_MORE_URL = "https://github.com/matrix-org/sliding-sync/blob/main/docs/Landing.md"
|
||||
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue