From 641e4f5881105878cd0d2bd7e4fe4de09d06054d Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 7 Jun 2023 14:18:46 +0200 Subject: [PATCH] Remove old screens. --- .../features/login/impl/LoginFlowNode.kt | 30 -- .../accountprovider/AccountProviderView.kt | 2 - .../SlidingSyncNotSupportedDialog.kt | 36 ++ .../impl/changeserver/ChangeServerEvents.kt | 23 - .../impl/changeserver/ChangeServerNode.kt | 65 --- .../changeserver/ChangeServerPresenter.kt | 77 ---- .../impl/changeserver/ChangeServerState.kt | 27 -- .../changeserver/ChangeServerStateProvider.kt | 44 -- .../impl/changeserver/ChangeServerView.kt | 292 ------------- .../login/impl/root/LoginRootEvents.kt | 25 -- .../features/login/impl/root/LoginRootNode.kt | 65 --- .../login/impl/root/LoginRootPresenter.kt | 171 -------- .../login/impl/root/LoginRootState.kt | 58 --- .../login/impl/root/LoginRootStateProvider.kt | 76 ---- .../features/login/impl/root/LoginRootView.kt | 403 ------------------ 15 files changed, 36 insertions(+), 1358 deletions(-) create mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/SlidingSyncNotSupportedDialog.kt delete mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerEvents.kt delete mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerNode.kt delete mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerPresenter.kt delete mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerState.kt delete mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerStateProvider.kt delete mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerView.kt delete mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootEvents.kt delete mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootNode.kt delete mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootPresenter.kt delete mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootState.kt delete mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootStateProvider.kt delete mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootView.kt diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/LoginFlowNode.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/LoginFlowNode.kt index 3220d8b897..1c0ab78ee4 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/LoginFlowNode.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/LoginFlowNode.kt @@ -36,13 +36,11 @@ import io.element.android.features.login.impl.accountprovider.AccountProviderNod import io.element.android.features.login.impl.accountprovider.item.AccountProvider import io.element.android.features.login.impl.changeaccountprovider.ChangeAccountProviderNode import io.element.android.features.login.impl.changeaccountprovider.form.ChangeAccountProviderFormNode -import io.element.android.features.login.impl.changeserver.ChangeServerNode import io.element.android.features.login.impl.datasource.AccountProviderDataSource import io.element.android.features.login.impl.loginpassword.LoginPasswordNode import io.element.android.features.login.impl.oidc.CustomTabAvailabilityChecker import io.element.android.features.login.impl.oidc.customtab.CustomTabHandler import io.element.android.features.login.impl.oidc.webview.OidcNode -import io.element.android.features.login.impl.root.LoginRootNode import io.element.android.libraries.architecture.BackstackNode import io.element.android.libraries.architecture.NodeInputs import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler @@ -78,10 +76,6 @@ class LoginFlowNode @AssistedInject constructor( private val inputs: Inputs = inputs() sealed interface NavTarget : Parcelable { - // Not used anymore - @Parcelize - object Root : NavTarget - @Parcelize object AccountProvider : NavTarget @@ -94,36 +88,12 @@ class LoginFlowNode @AssistedInject constructor( @Parcelize object LoginPasswordForm : NavTarget - // Not used anymore - @Parcelize - object ChangeServer : NavTarget - @Parcelize data class OidcView(val oidcDetails: OidcDetails) : NavTarget } override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { return when (navTarget) { - NavTarget.Root -> { - val callback = object : LoginRootNode.Callback { - override fun onChangeHomeServer() { - backstack.push(NavTarget.ChangeServer) - } - - override fun onOidcDetails(oidcDetails: OidcDetails) { - if (customTabAvailabilityChecker.supportCustomTab()) { - // In this case open a Chrome Custom tab - activity?.let { customTabHandler.open(it, darkTheme, oidcDetails.url) } - } else { - // Fallback to WebView mode - backstack.push(NavTarget.OidcView(oidcDetails)) - } - } - } - createNode(buildContext, plugins = listOf(callback)) - } - - NavTarget.ChangeServer -> createNode(buildContext) is NavTarget.OidcView -> { val input = OidcNode.Inputs(navTarget.oidcDetails) createNode(buildContext, plugins = listOf(input)) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderView.kt index 36352416f9..dcc219f93e 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderView.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderView.kt @@ -31,12 +31,10 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import io.element.android.features.login.impl.R import io.element.android.features.login.impl.error.ChangeServerError -import io.element.android.features.login.impl.changeserver.SlidingSyncNotSupportedDialog import io.element.android.libraries.architecture.Async import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage -import io.element.android.libraries.designsystem.components.async.AsyncFailure import io.element.android.libraries.designsystem.components.button.ButtonWithProgress import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog import io.element.android.libraries.designsystem.preview.ElementPreviewDark diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/SlidingSyncNotSupportedDialog.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/SlidingSyncNotSupportedDialog.kt new file mode 100644 index 0000000000..03cf3a754e --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/SlidingSyncNotSupportedDialog.kt @@ -0,0 +1,36 @@ +/* + * 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.accountprovider + +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import io.element.android.features.login.impl.R +import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog +import io.element.android.libraries.ui.strings.R as StringR + +@Composable +internal fun SlidingSyncNotSupportedDialog(onLearnMoreClicked: () -> Unit, onDismiss: () -> Unit) { + ConfirmationDialog( + onDismiss = onDismiss, + submitText = stringResource(StringR.string.action_learn_more), + onSubmitClicked = onLearnMoreClicked, + onCancelClicked = onDismiss, + emphasizeSubmitButton = true, + title = stringResource(StringR.string.dialog_title_error), + content = stringResource(R.string.screen_change_server_error_no_sliding_sync_message), + ) +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerEvents.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerEvents.kt deleted file mode 100644 index dd1e83c3f8..0000000000 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerEvents.kt +++ /dev/null @@ -1,23 +0,0 @@ -/* - * 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.changeserver - -sealed interface ChangeServerEvents { - data class SetServer(val server: String) : ChangeServerEvents - object Submit : ChangeServerEvents - object ClearError : ChangeServerEvents -} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerNode.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerNode.kt deleted file mode 100644 index 5ac13ed409..0000000000 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerNode.kt +++ /dev/null @@ -1,65 +0,0 @@ -/* - * 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.changeserver - -import android.content.Context -import android.content.Intent -import android.net.Uri -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 -import dagger.assisted.Assisted -import dagger.assisted.AssistedInject -import io.element.android.anvilannotations.ContributesNode -import io.element.android.features.login.impl.util.LoginConstants -import io.element.android.libraries.core.data.tryOrNull -import io.element.android.libraries.di.AppScope - -/** - * Not used anymore. - */ -@ContributesNode(AppScope::class) -class ChangeServerNode @AssistedInject constructor( - @Assisted buildContext: BuildContext, - @Assisted plugins: List, - private val presenter: ChangeServerPresenter, -) : Node(buildContext, plugins = plugins) { - private fun onSuccess() { - navigateUp() - } - - private fun openLearnMorePage(context: Context) { - val intent = Intent(Intent.ACTION_VIEW, Uri.parse(LoginConstants.SLIDING_SYNC_READ_MORE_URL)) - tryOrNull { context.startActivity(intent) } - } - - @Composable - override fun View(modifier: Modifier) { - val state = presenter.present() - val context = LocalContext.current - ChangeServerView( - state = state, - modifier = modifier, - onChangeServerSuccess = this::onSuccess, - onBackPressed = { navigateUp() }, - onLearnMoreClicked = { openLearnMorePage(context) }, - ) - } -} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerPresenter.kt deleted file mode 100644 index 81a19e5021..0000000000 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerPresenter.kt +++ /dev/null @@ -1,77 +0,0 @@ -/* - * 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.changeserver - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.MutableState -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.saveable.rememberSaveable -import io.element.android.features.login.impl.error.ChangeServerError -import io.element.android.features.login.impl.util.LoginConstants -import io.element.android.libraries.architecture.Async -import io.element.android.libraries.architecture.Presenter -import io.element.android.libraries.architecture.execute -import io.element.android.libraries.core.data.tryOrNull -import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch -import java.net.URL -import javax.inject.Inject - -class ChangeServerPresenter @Inject constructor(private val authenticationService: MatrixAuthenticationService) : Presenter { - - @Composable - override fun present(): ChangeServerState { - val localCoroutineScope = rememberCoroutineScope() - - val homeserver = rememberSaveable { - mutableStateOf(authenticationService.getHomeserverDetails().value?.url ?: LoginConstants.DEFAULT_HOMESERVER_URL) - } - val changeServerAction: MutableState> = remember { - mutableStateOf(Async.Uninitialized) - } - - fun handleEvents(event: ChangeServerEvents) { - when (event) { - is ChangeServerEvents.SetServer -> { - homeserver.value = event.server - handleEvents(ChangeServerEvents.ClearError) - } - ChangeServerEvents.Submit -> { - localCoroutineScope.submit(homeserver, changeServerAction) - } - ChangeServerEvents.ClearError -> changeServerAction.value = Async.Uninitialized - } - } - - return ChangeServerState( - homeserver = homeserver.value, - changeServerAction = changeServerAction.value, - eventSink = ::handleEvents - ) - } - - private fun CoroutineScope.submit(homeserverUrl: MutableState, changeServerAction: MutableState>) = launch { - suspend { - val domain = tryOrNull { URL(homeserverUrl.value) }?.host ?: homeserverUrl.value - authenticationService.setHomeserver(domain).getOrThrow() - homeserverUrl.value = domain - }.execute(changeServerAction, errorMapping = ChangeServerError::from) - } -} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerState.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerState.kt deleted file mode 100644 index 5a3ea3b856..0000000000 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerState.kt +++ /dev/null @@ -1,27 +0,0 @@ -/* - * 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.changeserver - -import io.element.android.libraries.architecture.Async - -data class ChangeServerState( - val homeserver: String, - val changeServerAction: Async, - val eventSink: (ChangeServerEvents) -> Unit, -) { - val submitEnabled: Boolean get() = homeserver.isNotEmpty() && (changeServerAction is Async.Uninitialized || changeServerAction is Async.Loading) -} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerStateProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerStateProvider.kt deleted file mode 100644 index 9e938ce15b..0000000000 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerStateProvider.kt +++ /dev/null @@ -1,44 +0,0 @@ -/* - * 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.changeserver - -import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import io.element.android.features.login.impl.R -import io.element.android.features.login.impl.error.ChangeServerError -import io.element.android.libraries.architecture.Async - -open class ChangeServerStateProvider : PreviewParameterProvider { - override val values: Sequence - get() = sequenceOf( - aChangeServerState(), - aChangeServerState().copy(homeserver = "matrix.org"), - aChangeServerState().copy(homeserver = "matrix.org", changeServerAction = Async.Loading()), - aChangeServerState().copy( - homeserver = "invalid.org", - changeServerAction = Async.Failure(ChangeServerError.InlineErrorMessage(R.string.screen_change_server_error_invalid_homeserver)) - ), - aChangeServerState().copy(homeserver = "invalid.org", changeServerAction = Async.Failure( - ChangeServerError.SlidingSyncAlert)), - aChangeServerState().copy(homeserver = "matrix.org", changeServerAction = Async.Success(Unit)), - ) -} - -fun aChangeServerState() = ChangeServerState( - homeserver = "", - changeServerAction = Async.Uninitialized, - eventSink = {} -) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerView.kt deleted file mode 100644 index 3d9a8d6545..0000000000 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerView.kt +++ /dev/null @@ -1,292 +0,0 @@ -/* - * Copyright (c) 2022 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.changeserver - -import androidx.compose.foundation.background -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ExperimentalLayoutApi -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.consumeWindowInsets -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.imePadding -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.text.KeyboardActions -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.foundation.verticalScroll -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Close -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.MaterialTheme -import androidx.compose.runtime.Composable -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalFocusManager -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.ExperimentalTextApi -import androidx.compose.ui.text.ParagraphStyle -import androidx.compose.ui.text.SpanStyle -import androidx.compose.ui.text.UrlAnnotation -import androidx.compose.ui.text.buildAnnotatedString -import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.withStyle -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.unit.dp -import io.element.android.features.login.impl.R -import io.element.android.features.login.impl.error.ChangeServerError -import io.element.android.features.login.impl.util.LoginConstants -import io.element.android.libraries.architecture.Async -import io.element.android.libraries.designsystem.ElementTextStyles -import io.element.android.libraries.designsystem.LinkColor -import io.element.android.libraries.designsystem.components.ClickableLinkText -import io.element.android.libraries.designsystem.components.button.BackButton -import io.element.android.libraries.designsystem.components.button.ButtonWithProgress -import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog -import io.element.android.libraries.designsystem.components.form.textFieldState -import io.element.android.libraries.designsystem.preview.ElementPreviewDark -import io.element.android.libraries.designsystem.preview.ElementPreviewLight -import io.element.android.libraries.designsystem.theme.LocalColors -import io.element.android.libraries.designsystem.theme.components.Icon -import io.element.android.libraries.designsystem.theme.components.IconButton -import io.element.android.libraries.designsystem.theme.components.Scaffold -import io.element.android.libraries.designsystem.theme.components.Text -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.onTabOrEnterKeyFocusNext -import io.element.android.libraries.testtags.TestTags -import io.element.android.libraries.testtags.testTag -import io.element.android.libraries.ui.strings.R as StringR - -@OptIn(ExperimentalMaterial3Api::class, ExperimentalTextApi::class, ExperimentalLayoutApi::class) -@Composable -fun ChangeServerView( - state: ChangeServerState, - onLearnMoreClicked: () -> Unit, - onBackPressed: () -> Unit, - modifier: Modifier = Modifier, - onChangeServerSuccess: () -> Unit = {}, -) { - val eventSink = state.eventSink - val scrollState = rememberScrollState() - val isLoading by remember(state.changeServerAction) { - derivedStateOf { - state.changeServerAction is Async.Loading - } - } - val invalidHomeserverError = (state.changeServerAction as? Async.Failure)?.error as? ChangeServerError.InlineErrorMessage - val slidingSyncNotSupportedError = (state.changeServerAction as? Async.Failure)?.error as? ChangeServerError.SlidingSyncAlert - val focusManager = LocalFocusManager.current - - fun submit() { - // Clear focus to prevent keyboard issues with textfields - focusManager.clearFocus(force = true) - - eventSink(ChangeServerEvents.Submit) - } - - Scaffold( - topBar = { - TopAppBar( - title = {}, - navigationIcon = { BackButton(onClick = onBackPressed) } - ) - } - ) { padding -> - Box( - modifier = modifier - .fillMaxSize() - .imePadding() - .padding(padding) - .consumeWindowInsets(padding) - ) { - Column( - modifier = Modifier - .verticalScroll( - state = scrollState, - ) - .padding(horizontal = 16.dp) - ) { - Spacer(Modifier.height(42.dp)) - Box( - modifier = Modifier - .size(width = 70.dp, height = 70.dp) - .align(Alignment.CenterHorizontally) - .background( - color = LocalColors.current.quinary, - shape = RoundedCornerShape(14.dp) - ) - ) { - Icon( - modifier = Modifier - .align(Alignment.Center) - .size(width = 32.dp, height = 32.dp), - tint = MaterialTheme.colorScheme.secondary, - resourceId = R.drawable.ic_homeserver, - contentDescription = "", - ) - } - Spacer(modifier = Modifier.height(16.dp)) - Text( - text = stringResource(id = R.string.screen_change_server_title), - modifier = Modifier - .fillMaxWidth() - .align(Alignment.CenterHorizontally), - textAlign = TextAlign.Center, - style = ElementTextStyles.Bold.title2, - color = MaterialTheme.colorScheme.primary, - ) - Spacer(Modifier.height(8.dp)) - Text( - text = stringResource(id = R.string.screen_change_server_subtitle), - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - textAlign = TextAlign.Center, - style = ElementTextStyles.Regular.subheadline, - color = MaterialTheme.colorScheme.secondary, - ) - Spacer(Modifier.height(24.dp)) - Text( - stringResource(R.string.screen_change_server_form_header), - style = ElementTextStyles.Regular.formHeader, - modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp) - ) - var homeserverFieldState by textFieldState(stateValue = state.homeserver) - TextField( - value = homeserverFieldState, - readOnly = isLoading, - modifier = Modifier - .fillMaxWidth() - .testTag(TestTags.changeServerServer) - .onTabOrEnterKeyFocusNext(focusManager), - onValueChange = { - homeserverFieldState = it - eventSink(ChangeServerEvents.SetServer(it)) - }, - keyboardOptions = KeyboardOptions( - keyboardType = KeyboardType.Password, - imeAction = ImeAction.Done, - ), - keyboardActions = KeyboardActions( - onDone = { submit() } - ), - singleLine = true, - maxLines = 1, - trailingIcon = if (homeserverFieldState.isNotEmpty()) { - { - IconButton(onClick = { - eventSink(ChangeServerEvents.SetServer("")) - }, enabled = !isLoading) { - Icon(imageVector = Icons.Filled.Close, contentDescription = stringResource(StringR.string.action_clear)) - } - } - } else null, - isError = invalidHomeserverError != null, - supportingText = { - if (invalidHomeserverError != null) { - Text(invalidHomeserverError.message(), color = MaterialTheme.colorScheme.error) - } else { - val footerMessage = stringResource(R.string.screen_change_server_form_notice, "") - val footerAction = stringResource(StringR.string.action_learn_more) - val footerText = buildAnnotatedString { - val defaultColor = MaterialTheme.colorScheme.tertiary - withStyle(ParagraphStyle(textAlign = TextAlign.Start)) { - withStyle(SpanStyle(color = defaultColor)) { - append(footerMessage) - } - val start = length - withStyle(SpanStyle(color = LinkColor)) { - append(footerAction) - } - addUrlAnnotation(UrlAnnotation(LoginConstants.SLIDING_SYNC_READ_MORE_URL), start, length) - } - } - ClickableLinkText( - text = footerText, - interactionSource = MutableInteractionSource(), - style = ElementTextStyles.Regular.caption1, - ) - } - } - - ) - if (slidingSyncNotSupportedError != null) { - SlidingSyncNotSupportedDialog(onLearnMoreClicked = { - onLearnMoreClicked() - eventSink(ChangeServerEvents.ClearError) - }, onDismiss = { - eventSink(ChangeServerEvents.ClearError) - }) - } - Spacer(Modifier.height(32.dp)) - ButtonWithProgress( - text = stringResource(id = R.string.screen_change_server_submit), - showProgress = isLoading, - onClick = ::submit, - enabled = state.submitEnabled, - modifier = Modifier - .fillMaxWidth() - .testTag(TestTags.changeServerContinue) - ) - if (state.changeServerAction is Async.Success) { - onChangeServerSuccess() - } - } - } - } -} - -@Composable -internal fun SlidingSyncNotSupportedDialog(onLearnMoreClicked: () -> Unit, onDismiss: () -> Unit) { - ConfirmationDialog( - onDismiss = onDismiss, - submitText = stringResource(StringR.string.action_learn_more), - onSubmitClicked = onLearnMoreClicked, - onCancelClicked = onDismiss, - emphasizeSubmitButton = true, - title = stringResource(StringR.string.dialog_title_error), - content = stringResource(R.string.screen_change_server_error_no_sliding_sync_message), - ) -} - -@Preview -@Composable -internal fun ChangeServerViewLightPreview(@PreviewParameter(ChangeServerStateProvider::class) state: ChangeServerState) = - ElementPreviewLight { ContentToPreview(state) } - -@Preview -@Composable -internal fun ChangeServerViewDarkPreview(@PreviewParameter(ChangeServerStateProvider::class) state: ChangeServerState) = - ElementPreviewDark { ContentToPreview(state) } - -@Composable -private fun ContentToPreview(state: ChangeServerState) { - ChangeServerView(state = state, onBackPressed = {}, onLearnMoreClicked = {}) -} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootEvents.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootEvents.kt deleted file mode 100644 index 5aa5071876..0000000000 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootEvents.kt +++ /dev/null @@ -1,25 +0,0 @@ -/* - * 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.root - -sealed interface LoginRootEvents { - object RetryFetchServerInfo : LoginRootEvents - data class SetLogin(val login: String) : LoginRootEvents - data class SetPassword(val password: String) : LoginRootEvents - object Submit : LoginRootEvents - object ClearError : LoginRootEvents -} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootNode.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootNode.kt deleted file mode 100644 index 1de7477555..0000000000 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootNode.kt +++ /dev/null @@ -1,65 +0,0 @@ -/* - * 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.root - -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 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.di.AppScope -import io.element.android.libraries.matrix.api.auth.OidcDetails - -/** - * Not used anymore. - */ -@ContributesNode(AppScope::class) -class LoginRootNode @AssistedInject constructor( - @Assisted buildContext: BuildContext, - @Assisted plugins: List, - private val presenter: LoginRootPresenter, -) : Node(buildContext, plugins = plugins) { - - interface Callback : Plugin { - fun onChangeHomeServer() - fun onOidcDetails(oidcDetails: OidcDetails) - } - - private fun onChangeHomeServer() { - plugins().forEach { it.onChangeHomeServer() } - } - - private fun onOidcDetails(oidcDetails: OidcDetails) { - plugins().forEach { it.onOidcDetails(oidcDetails) } - } - - @Composable - override fun View(modifier: Modifier) { - val state = presenter.present() - LoginRootView( - state = state, - modifier = modifier, - onChangeServer = ::onChangeHomeServer, - onOidcDetails = ::onOidcDetails, - onBackPressed = ::navigateUp - ) - } -} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootPresenter.kt deleted file mode 100644 index f55c2030e7..0000000000 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootPresenter.kt +++ /dev/null @@ -1,171 +0,0 @@ -/* - * 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.root - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.MutableState -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.saveable.rememberSaveable -import io.element.android.features.login.api.oidc.OidcAction -import io.element.android.features.login.impl.oidc.customtab.DefaultOidcActionFlow -import io.element.android.features.login.impl.util.LoginConstants -import io.element.android.libraries.architecture.Async -import io.element.android.libraries.architecture.Presenter -import io.element.android.libraries.architecture.execute -import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService -import io.element.android.libraries.matrix.api.auth.MatrixHomeServerDetails -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch -import javax.inject.Inject - -class LoginRootPresenter @Inject constructor( - private val authenticationService: MatrixAuthenticationService, - private val defaultOidcActionFlow: DefaultOidcActionFlow, -) : Presenter { - - @Composable - override fun present(): LoginRootState { - val localCoroutineScope = rememberCoroutineScope() - val currentHomeServerDetails = authenticationService.getHomeserverDetails().collectAsState().value - val homeserver = currentHomeServerDetails?.url ?: LoginConstants.DEFAULT_HOMESERVER_URL - val getHomeServerDetailsAction: MutableState> = remember { - if (currentHomeServerDetails != null) { - mutableStateOf(Async.Success(currentHomeServerDetails)) - } else { - mutableStateOf(Async.Uninitialized) - } - } - - LaunchedEffect(Unit) { - if (currentHomeServerDetails == null) { - getHomeServerDetails(homeserver, getHomeServerDetailsAction) - } - } - - val loggedInState: MutableState = remember { - mutableStateOf(LoggedInState.NotLoggedIn) - } - val formState = rememberSaveable { - mutableStateOf(LoginFormState.Default) - } - - LaunchedEffect(Unit) { - launch { - defaultOidcActionFlow.collect { - onOidcAction(it, loggedInState) - } - } - } - - fun handleEvents(event: LoginRootEvents) { - when (event) { - LoginRootEvents.RetryFetchServerInfo -> localCoroutineScope.getHomeServerDetails(homeserver, getHomeServerDetailsAction) - is LoginRootEvents.SetLogin -> updateFormState(formState) { - copy(login = event.login) - } - is LoginRootEvents.SetPassword -> updateFormState(formState) { - copy(password = event.password) - } - LoginRootEvents.Submit -> { - val homeServerDetails = getHomeServerDetailsAction.value.dataOrNull() ?: return - when { - homeServerDetails.supportsOidcLogin -> localCoroutineScope.submitOidc(loggedInState) - homeServerDetails.supportsPasswordLogin -> localCoroutineScope.submit(formState.value, loggedInState) - } - } - LoginRootEvents.ClearError -> loggedInState.value = LoggedInState.NotLoggedIn - } - } - - return LoginRootState( - homeserverUrl = homeserver, - homeserverDetails = getHomeServerDetailsAction.value, - loggedInState = loggedInState.value, - formState = formState.value, - eventSink = ::handleEvents - ) - } - - private fun CoroutineScope.getHomeServerDetails( - homeserver: String, - state: MutableState>, - ) = launch { - suspend { - authenticationService.setHomeserver(homeserver) - .map { - authenticationService.getHomeserverDetails().value!! - } - .getOrThrow() - }.execute(state) - } - - private fun CoroutineScope.submitOidc(loggedInState: MutableState) = launch { - loggedInState.value = LoggedInState.LoggingIn - authenticationService.getOidcUrl() - .onSuccess { - loggedInState.value = LoggedInState.OidcStarted(it) - } - .onFailure { failure -> - loggedInState.value = LoggedInState.ErrorLoggingIn(failure) - } - } - - private fun CoroutineScope.submit(formState: LoginFormState, loggedInState: MutableState) = launch { - loggedInState.value = LoggedInState.LoggingIn - authenticationService.login(formState.login.trim(), formState.password) - .onSuccess { sessionId -> - loggedInState.value = LoggedInState.LoggedIn(sessionId) - } - .onFailure { failure -> - loggedInState.value = LoggedInState.ErrorLoggingIn(failure) - } - } - - private fun updateFormState(formState: MutableState, updateLambda: LoginFormState.() -> LoginFormState) { - formState.value = updateLambda(formState.value) - } - - private suspend fun onOidcAction(oidcAction: OidcAction?, loggedInState: MutableState) { - oidcAction ?: return - loggedInState.value = LoggedInState.LoggingIn - when (oidcAction) { - OidcAction.GoBack -> { - authenticationService.cancelOidcLogin() - .onSuccess { - loggedInState.value = LoggedInState.NotLoggedIn - } - .onFailure { failure -> - loggedInState.value = LoggedInState.ErrorLoggingIn(failure) - } - } - is OidcAction.Success -> { - authenticationService.loginWithOidc(oidcAction.url) - .onSuccess { sessionId -> - loggedInState.value = LoggedInState.LoggedIn(sessionId) - } - .onFailure { failure -> - loggedInState.value = LoggedInState.ErrorLoggingIn(failure) - } - } - } - defaultOidcActionFlow.reset() - } -} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootState.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootState.kt deleted file mode 100644 index 45eafa744c..0000000000 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootState.kt +++ /dev/null @@ -1,58 +0,0 @@ -/* - * 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.root - -import android.os.Parcelable -import io.element.android.libraries.architecture.Async -import io.element.android.libraries.core.bool.orFalse -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 - -data class LoginRootState( - val homeserverUrl: String, - val homeserverDetails: Async, - val loggedInState: LoggedInState, - val formState: LoginFormState, - val eventSink: (LoginRootEvents) -> Unit -) { - val supportPasswordLogin = (homeserverDetails as? Async.Success)?.state?.supportsPasswordLogin.orFalse() - val supportOidcLogin = (homeserverDetails as? Async.Success)?.state?.supportsOidcLogin.orFalse() - 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 -} - -@Parcelize -data class LoginFormState( - val login: String, - val password: String -) : Parcelable { - - companion object { - val Default = LoginFormState("", "") - } -} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootStateProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootStateProvider.kt deleted file mode 100644 index 5f6d7c1f3a..0000000000 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootStateProvider.kt +++ /dev/null @@ -1,76 +0,0 @@ -/* - * 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.root - -import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import io.element.android.libraries.architecture.Async -import io.element.android.libraries.matrix.api.auth.MatrixHomeServerDetails -import io.element.android.libraries.matrix.api.core.SessionId - -open class LoginRootStateProvider : PreviewParameterProvider { - override val values: Sequence - get() = sequenceOf( - aLoginRootState(), - aLoginRootState().copy( - homeserverDetails = Async.Success( - MatrixHomeServerDetails( - "some-custom-server.com", - supportsPasswordLogin = true, - supportsOidcLogin = 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( - homeserverUrl = "server-with-oidc.org", - homeserverDetails = Async.Success( - MatrixHomeServerDetails( - "server-with-oidc.org", - supportsPasswordLogin = false, - supportsOidcLogin = true - ) - ) - ), - // No password, no oidc support - aLoginRootState().copy( - homeserverUrl = "wrong.org", - homeserverDetails = Async.Success( - MatrixHomeServerDetails( - "wrong.org", - supportsPasswordLogin = false, - supportsOidcLogin = false - ) - ) - ), - // Loading - aLoginRootState().copy(homeserverDetails = Async.Loading()), - //Error - aLoginRootState().copy(homeserverDetails = Async.Failure(Exception("An error occurred"))), - ) -} - -fun aLoginRootState() = LoginRootState( - homeserverUrl = "matrix.org", - homeserverDetails = Async.Success(MatrixHomeServerDetails("matrix.org", supportsPasswordLogin = true, supportsOidcLogin = false)), - loggedInState = LoggedInState.NotLoggedIn, - formState = LoginFormState.Default, - eventSink = {} -) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootView.kt deleted file mode 100644 index 2444418725..0000000000 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootView.kt +++ /dev/null @@ -1,403 +0,0 @@ -/* - * Copyright (c) 2022 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.root - -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ExperimentalLayoutApi -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.consumeWindowInsets -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.imePadding -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.text.KeyboardActions -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.foundation.verticalScroll -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.ChevronRight -import androidx.compose.material.icons.filled.Close -import androidx.compose.material.icons.filled.Visibility -import androidx.compose.material.icons.filled.VisibilityOff -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.MaterialTheme -import androidx.compose.runtime.Composable -import androidx.compose.runtime.derivedStateOf -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.ExperimentalComposeUiApi -import androidx.compose.ui.Modifier -import androidx.compose.ui.autofill.AutofillType -import androidx.compose.ui.draw.clip -import androidx.compose.ui.focus.FocusDirection -import androidx.compose.ui.platform.LocalFocusManager -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.text.input.PasswordVisualTransformation -import androidx.compose.ui.text.input.VisualTransformation -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.unit.dp -import io.element.android.features.login.impl.R -import io.element.android.features.login.impl.error.loginError -import io.element.android.libraries.architecture.Async -import io.element.android.libraries.designsystem.ElementTextStyles -import io.element.android.libraries.designsystem.components.async.AsyncFailure -import io.element.android.libraries.designsystem.components.async.AsyncLoading -import io.element.android.libraries.designsystem.components.button.BackButton -import io.element.android.libraries.designsystem.components.button.ButtonWithProgress -import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog -import io.element.android.libraries.designsystem.components.form.textFieldState -import io.element.android.libraries.designsystem.preview.ElementPreviewDark -import io.element.android.libraries.designsystem.preview.ElementPreviewLight -import io.element.android.libraries.designsystem.theme.components.Icon -import io.element.android.libraries.designsystem.theme.components.IconButton -import io.element.android.libraries.designsystem.theme.components.Scaffold -import io.element.android.libraries.designsystem.theme.components.Text -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.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 - -@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class) -@Composable -fun LoginRootView( - state: LoginRootState, - modifier: Modifier = Modifier, - onChangeServer: () -> Unit = {}, - onOidcDetails: (OidcDetails) -> Unit = {}, - onBackPressed: () -> Unit, -) { - val isLoading by remember(state.loggedInState) { - derivedStateOf { - 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( - title = {}, - navigationIcon = { BackButton(onClick = onBackPressed) }, - ) - } - ) { padding -> - Box( - modifier = modifier - .fillMaxSize() - .imePadding() - .padding(padding) - .consumeWindowInsets(padding) - ) { - val scrollState = rememberScrollState() - - Column( - modifier = Modifier - .verticalScroll(state = scrollState) - .padding(horizontal = 16.dp), - ) { - Spacer(Modifier.height(16.dp)) - // Title - Text( - text = stringResource(id = R.string.screen_login_title), - modifier = Modifier - .fillMaxWidth(), - style = ElementTextStyles.Bold.title1, - color = MaterialTheme.colorScheme.primary, - ) - Spacer(Modifier.height(32.dp)) - - ChangeServerSection( - interactionEnabled = !isLoading, - homeserver = state.homeserverUrl, - onChangeServer = onChangeServer - ) - - Spacer(Modifier.height(32.dp)) - - when (state.homeserverDetails) { - Async.Uninitialized, - is Async.Loading -> AsyncLoading() - is Async.Failure -> AsyncFailure( - throwable = state.homeserverDetails.error, - onRetry = { - state.eventSink.invoke(LoginRootEvents.RetryFetchServerInfo) - } - ) - is Async.Success -> ServerDetailForm(state, isLoading, ::submit) - } - } - when (val loggedInState = state.loggedInState) { - is LoggedInState.OidcStarted -> onOidcDetails(loggedInState.oidcDetail) - else -> Unit - } - } - } - - if (state.loggedInState is LoggedInState.ErrorLoggingIn) { - LoginErrorDialog(error = state.loggedInState.failure, onDismiss = { - state.eventSink(LoginRootEvents.ClearError) - }) - } -} - -@Composable -fun ServerDetailForm( - state: LoginRootState, - isLoading: Boolean, - submit: () -> Unit, - modifier: Modifier = Modifier, -) { - 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, modifier = modifier) - } - else -> { - Text(modifier = modifier, text = "No supported login flow") - } - } - - 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)) - } -} - -@Composable -internal fun ChangeServerSection( - interactionEnabled: Boolean, - homeserver: String, - onChangeServer: () -> Unit, - modifier: Modifier = Modifier -) { - Column(modifier) { - Text( - modifier = Modifier.padding(start = 16.dp, bottom = 8.dp), - text = stringResource(id = R.string.screen_login_server_header), - style = ElementTextStyles.Regular.formHeader, - ) - Row( - modifier = Modifier - .fillMaxWidth() - .clip(RoundedCornerShape(14.dp)) - .background(MaterialTheme.colorScheme.surfaceVariant) - .testTag(TestTags.loginChangeServer) - .clickable { - if (interactionEnabled) { - onChangeServer() - } - }, - horizontalArrangement = Arrangement.End, - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - text = homeserver, - style = ElementTextStyles.Bold.body, - textAlign = TextAlign.Start, - modifier = Modifier - .weight(1f) - .padding(horizontal = 16.dp, vertical = 16.dp) - ) - IconButton( - modifier = Modifier.size(24.dp), - onClick = { - if (interactionEnabled) { - onChangeServer() - } - } - ) { - Icon(imageVector = Icons.Default.ChevronRight, contentDescription = null, tint = MaterialTheme.colorScheme.tertiary) - } - Spacer(Modifier.width(8.dp)) - } - } -} - -@OptIn(ExperimentalComposeUiApi::class) -@Composable -internal fun LoginForm( - state: LoginRootState, - isLoading: Boolean, - onSubmit: () -> Unit, - modifier: Modifier = Modifier -) { - var loginFieldState by textFieldState(stateValue = state.formState.login) - var passwordFieldState by textFieldState(stateValue = state.formState.password) - - val focusManager = LocalFocusManager.current - val eventSink = state.eventSink - - Column(modifier) { - Text( - text = stringResource(R.string.screen_login_form_header), - modifier = Modifier.padding(start = 16.dp), - style = ElementTextStyles.Regular.formHeader - ) - - Spacer(modifier = Modifier.height(8.dp)) - TextField( - value = loginFieldState, - readOnly = isLoading, - modifier = Modifier - .fillMaxWidth() - .onTabOrEnterKeyFocusNext(focusManager) - .testTag(TestTags.loginEmailUsername) - .autofill(autofillTypes = listOf(AutofillType.Username), onFill = { - loginFieldState = it - eventSink(LoginRootEvents.SetLogin(it)) - }), - label = { - Text(text = stringResource(R.string.screen_login_username_hint)) - }, - onValueChange = { - loginFieldState = it - eventSink(LoginRootEvents.SetLogin(it)) - }, - keyboardOptions = KeyboardOptions( - keyboardType = KeyboardType.Email, - imeAction = ImeAction.Next - ), - keyboardActions = KeyboardActions(onNext = { - focusManager.moveFocus(FocusDirection.Down) - }), - singleLine = true, - maxLines = 1, - trailingIcon = if (loginFieldState.isNotEmpty()) { - { - IconButton(onClick = { - loginFieldState = "" - }) { - Icon(imageVector = Icons.Filled.Close, contentDescription = stringResource(StringR.string.action_clear)) - } - } - } else null, - ) - - var passwordVisible by remember { mutableStateOf(false) } - if (state.loggedInState is LoggedInState.LoggingIn) { - // Ensure password is hidden when user submits the form - passwordVisible = false - } - Spacer(Modifier.height(20.dp)) - TextField( - value = passwordFieldState, - readOnly = isLoading, - modifier = Modifier - .fillMaxWidth() - .onTabOrEnterKeyFocusNext(focusManager) - .testTag(TestTags.loginPassword) - .autofill(autofillTypes = listOf(AutofillType.Password), onFill = { - passwordFieldState = it - eventSink(LoginRootEvents.SetPassword(it)) - }), - onValueChange = { - passwordFieldState = it - eventSink(LoginRootEvents.SetPassword(it)) - }, - label = { - Text(text = stringResource(R.string.screen_login_password_hint)) - }, - visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(), - trailingIcon = { - val image = - if (passwordVisible) Icons.Filled.Visibility else Icons.Filled.VisibilityOff - val description = - if (passwordVisible) stringResource(StringR.string.a11y_hide_password) else stringResource(StringR.string.a11y_show_password) - - IconButton(onClick = { passwordVisible = !passwordVisible }) { - Icon(imageVector = image, description) - } - }, - keyboardOptions = KeyboardOptions( - keyboardType = KeyboardType.Password, - imeAction = ImeAction.Done, - ), - keyboardActions = KeyboardActions( - onDone = { onSubmit() } - ), - singleLine = true, - maxLines = 1, - ) - } -} - -@Composable -internal fun LoginErrorDialog(error: Throwable, onDismiss: () -> Unit) { - ErrorDialog( - content = stringResource(loginError(error)), - onDismiss = onDismiss - ) -} - -@Preview -@Composable -internal fun LoginRootScreenLightPreview(@PreviewParameter(LoginRootStateProvider::class) state: LoginRootState) = - ElementPreviewLight { ContentToPreview(state) } - -@Preview -@Composable -internal fun LoginRootScreenDarkPreview(@PreviewParameter(LoginRootStateProvider::class) state: LoginRootState) = - ElementPreviewDark { ContentToPreview(state) } - -@Composable -private fun ContentToPreview(state: LoginRootState) { - LoginRootView( - state = state, - onBackPressed = {} - ) -}