Rename nodes and organize by package into screens subpackage for clarity

This commit is contained in:
Benoit Marty
2023-06-09 17:34:14 +02:00
parent 06297bb792
commit dc064069ba
52 changed files with 450 additions and 476 deletions

View File

@@ -32,14 +32,14 @@ import com.bumble.appyx.navmodel.backstack.operation.singleTop
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode
import io.element.android.features.login.impl.accountprovider.AccountProviderNode
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.datasource.AccountProviderDataSource
import io.element.android.features.login.impl.loginpassword.LoginPasswordNode
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
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.screens.changeaccountprovider.ChangeAccountProviderNode
import io.element.android.features.login.impl.screens.confirmaccountprovider.ConfirmAccountProviderNode
import io.element.android.features.login.impl.screens.loginpassword.LoginPasswordNode
import io.element.android.features.login.impl.screens.searchaccountprovider.SearchAccountProviderNode
import io.element.android.libraries.architecture.BackstackNode
import io.element.android.libraries.architecture.NodeInputs
import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler
@@ -59,7 +59,7 @@ class LoginFlowNode @AssistedInject constructor(
private val accountProviderDataSource: AccountProviderDataSource,
) : BackstackNode<LoginFlowNode.NavTarget>(
backstack = BackStack(
initialElement = NavTarget.AccountProvider, // NavTarget.Root,
initialElement = NavTarget.ConfirmAccountProvider,
savedStateMap = buildContext.savedStateMap,
),
buildContext = buildContext,
@@ -76,16 +76,16 @@ class LoginFlowNode @AssistedInject constructor(
sealed interface NavTarget : Parcelable {
@Parcelize
object AccountProvider : NavTarget
object ConfirmAccountProvider : NavTarget
@Parcelize
object ChangeAccountProvider : NavTarget
@Parcelize
object ChangeAccountProviderForm : NavTarget
object SearchAccountProvider : NavTarget
@Parcelize
object LoginPasswordForm : NavTarget
object LoginPassword : NavTarget
@Parcelize
data class OidcView(val oidcDetails: OidcDetails) : NavTarget
@@ -93,15 +93,11 @@ class LoginFlowNode @AssistedInject constructor(
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
return when (navTarget) {
is NavTarget.OidcView -> {
val input = OidcNode.Inputs(navTarget.oidcDetails)
createNode<OidcNode>(buildContext, plugins = listOf(input))
}
NavTarget.AccountProvider -> {
val inputs = AccountProviderNode.Inputs(
NavTarget.ConfirmAccountProvider -> {
val inputs = ConfirmAccountProviderNode.Inputs(
isAccountCreation = inputs.isAccountCreation
)
val callback = object : AccountProviderNode.Callback {
val callback = object : ConfirmAccountProviderNode.Callback {
override fun onOidcDetails(oidcDetails: OidcDetails) {
if (customTabAvailabilityChecker.supportCustomTab()) {
// In this case open a Chrome Custom tab
@@ -113,42 +109,46 @@ class LoginFlowNode @AssistedInject constructor(
}
override fun onLoginPasswordNeeded() {
backstack.push(NavTarget.LoginPasswordForm)
backstack.push(NavTarget.LoginPassword)
}
override fun onChangeAccountProvider() {
backstack.push(NavTarget.ChangeAccountProvider)
}
}
createNode<AccountProviderNode>(buildContext, plugins = listOf(inputs, callback))
createNode<ConfirmAccountProviderNode>(buildContext, plugins = listOf(inputs, callback))
}
NavTarget.ChangeAccountProvider -> {
val callback = object : ChangeAccountProviderNode.Callback {
override fun onDone() {
// Go back to the Account Provider screen
backstack.singleTop(NavTarget.AccountProvider)
backstack.singleTop(NavTarget.ConfirmAccountProvider)
}
override fun onOtherClicked() {
backstack.push(NavTarget.ChangeAccountProviderForm)
backstack.push(NavTarget.SearchAccountProvider)
}
}
createNode<ChangeAccountProviderNode>(buildContext, plugins = listOf(callback))
}
NavTarget.ChangeAccountProviderForm -> {
val callback = object : ChangeAccountProviderFormNode.Callback {
NavTarget.SearchAccountProvider -> {
val callback = object : SearchAccountProviderNode.Callback {
override fun onDone() {
// Go back to the Account Provider screen
backstack.singleTop(NavTarget.AccountProvider)
backstack.singleTop(NavTarget.ConfirmAccountProvider)
}
}
createNode<ChangeAccountProviderFormNode>(buildContext, plugins = listOf(callback))
createNode<SearchAccountProviderNode>(buildContext, plugins = listOf(callback))
}
NavTarget.LoginPasswordForm -> {
NavTarget.LoginPassword -> {
createNode<LoginPasswordNode>(buildContext, plugins = listOf())
}
is NavTarget.OidcView -> {
val input = OidcNode.Inputs(navTarget.oidcDetails)
createNode<OidcNode>(buildContext, plugins = listOf(input))
}
}
}

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.features.login.impl.accountprovider.item
package io.element.android.features.login.impl.accountprovider
data class AccountProvider constructor(
val title: String,

View File

@@ -14,9 +14,8 @@
* limitations under the License.
*/
package io.element.android.features.login.impl.datasource
package io.element.android.features.login.impl.accountprovider
import io.element.android.features.login.impl.accountprovider.item.AccountProvider
import io.element.android.features.login.impl.util.defaultAccountProvider
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.SingleIn

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.features.login.impl.accountprovider.item
package io.element.android.features.login.impl.accountprovider
import androidx.compose.ui.tooling.preview.PreviewParameterProvider

View File

@@ -16,146 +16,117 @@
package io.element.android.features.login.impl.accountprovider
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AccountCircle
import androidx.compose.material.icons.filled.Search
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.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.graphics.Color
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.ChangeServerError
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.button.ButtonWithProgress
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
import io.element.android.libraries.designsystem.ElementTextStyles
import io.element.android.libraries.designsystem.atomic.atoms.RoundedIconAtom
import io.element.android.libraries.designsystem.atomic.atoms.RoundedIconAtomSize
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.theme.components.Divider
import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.components.TextButton
import io.element.android.libraries.matrix.api.auth.OidcDetails
import io.element.android.libraries.testtags.TestTags
import io.element.android.libraries.testtags.testTag
/**
* https://www.figma.com/file/o9p34zmiuEpZRyvZXJZAYL/FTUE?type=design&node-id=604-60817
*/
@Composable
fun AccountProviderView(
state: AccountProviderState,
item: AccountProvider,
modifier: Modifier = Modifier,
onOidcDetails: (OidcDetails) -> Unit = {},
onLoginPasswordNeeded: () -> Unit = {},
onLearnMoreClicked: () -> Unit = {},
onChange: () -> Unit = {},
onClick: () -> Unit,
) {
val isLoading by remember(state.loginFlow) {
derivedStateOf {
state.loginFlow is Async.Loading
}
}
val eventSink = state.eventSink
HeaderFooterPage(
modifier = modifier,
header = {
IconTitleSubtitleMolecule(
modifier = Modifier.padding(top = 60.dp),
iconImageVector = Icons.Filled.AccountCircle,
title = stringResource(
id = if (state.isAccountCreation) {
R.string.screen_account_provider_signup_title
} else {
R.string.screen_account_provider_signin_title
},
state.accountProvider.title
),
subTitle = stringResource(
id = if (state.isAccountCreation) {
R.string.screen_account_provider_signup_subtitle
} else {
// Use same value for now.
R.string.screen_account_provider_signup_subtitle
},
)
)
},
footer = {
ButtonColumnMolecule {
ButtonWithProgress(
text = stringResource(id = R.string.screen_account_provider_continue),
showProgress = isLoading,
onClick = { eventSink.invoke(AccountProviderEvents.Continue) },
enabled = state.submitEnabled,
Column(modifier = modifier
.fillMaxWidth()
.clickable { onClick() }) {
Divider()
Column(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 4.dp, horizontal = 16.dp)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.heightIn(min = 44.dp),
verticalAlignment = Alignment.CenterVertically
) {
if (item.isMatrixOrg) {
RoundedIconAtom(
size = RoundedIconAtomSize.Medium,
resourceId = R.drawable.ic_matrix,
tint = Color.Unspecified,
)
} else {
RoundedIconAtom(
size = RoundedIconAtomSize.Medium,
imageVector = Icons.Filled.Search,
tint = MaterialTheme.colorScheme.primary,
)
}
Text(
modifier = Modifier
.fillMaxWidth()
.testTag(TestTags.loginContinue)
.padding(start = 16.dp)
.weight(1f),
text = item.title,
style = ElementTextStyles.Regular.headline.copy(textAlign = TextAlign.Start),
color = MaterialTheme.colorScheme.primary,
)
TextButton(
onClick = {
onChange()
},
enabled = true,
if (item.isPublic) {
Icon(
modifier = Modifier
.padding(start = 10.dp)
.size(16.dp),
resourceId = R.drawable.ic_public,
contentDescription = null,
tint = Color.Unspecified,
)
}
}
if (item.subtitle != null) {
Text(
modifier = Modifier
.fillMaxWidth()
.testTag(TestTags.loginChangeServer)
) {
Text(text = stringResource(id = R.string.screen_account_provider_change))
}
.padding(start = 46.dp, bottom = 12.dp, end = 26.dp),
text = item.subtitle,
style = ElementTextStyles.Regular.subheadline.copy(textAlign = TextAlign.Start),
color = MaterialTheme.colorScheme.secondary,
)
}
}
) {
when (state.loginFlow) {
is Async.Failure -> {
when (val error = state.loginFlow.error) {
is ChangeServerError.InlineErrorMessage -> {
ErrorDialog(
content = error.message(),
onDismiss = {
eventSink.invoke(AccountProviderEvents.ClearError)
}
)
}
is ChangeServerError.SlidingSyncAlert -> {
SlidingSyncNotSupportedDialog(onLearnMoreClicked = {
onLearnMoreClicked()
eventSink(AccountProviderEvents.ClearError)
}, onDismiss = {
eventSink(AccountProviderEvents.ClearError)
})
}
}
}
is Async.Loading -> Unit // The Continue button shows the loading state
is Async.Success -> {
when (val loginFlowState = state.loginFlow.state) {
is LoginFlow.OidcFlow -> onOidcDetails(loginFlowState.oidcDetails)
LoginFlow.PasswordLogin -> onLoginPasswordNeeded()
}
}
Async.Uninitialized -> Unit
}
}
}
@Preview
@Composable
fun AccountProviderViewLightPreview(@PreviewParameter(AccountProviderStateProvider::class) state: AccountProviderState) =
ElementPreviewLight { ContentToPreview(state) }
fun AccountProviderViewLightPreview(@PreviewParameter(AccountProviderProvider::class) item: AccountProvider) =
ElementPreviewLight { ContentToPreview(item) }
@Preview
@Composable
fun AccountProviderViewDarkPreview(@PreviewParameter(AccountProviderStateProvider::class) state: AccountProviderState) =
ElementPreviewDark { ContentToPreview(state) }
fun AccountProviderViewDarkPreview(@PreviewParameter(AccountProviderProvider::class) item: AccountProvider) =
ElementPreviewDark { ContentToPreview(item) }
@Composable
private fun ContentToPreview(state: AccountProviderState) {
private fun ContentToPreview(item: AccountProvider) {
AccountProviderView(
state = state,
item = item,
onClick = { }
)
}

View File

@@ -1,132 +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.accountprovider.item
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Search
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
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.libraries.designsystem.ElementTextStyles
import io.element.android.libraries.designsystem.atomic.atoms.RoundedIconAtom
import io.element.android.libraries.designsystem.atomic.atoms.RoundedIconAtomSize
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.theme.components.Divider
import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.Text
/**
* https://www.figma.com/file/o9p34zmiuEpZRyvZXJZAYL/FTUE?type=design&node-id=604-60817
*/
@Composable
fun AccountProviderView(
item: AccountProvider,
modifier: Modifier = Modifier,
onClick: () -> Unit,
) {
Column(modifier = modifier
.fillMaxWidth()
.clickable { onClick() }) {
Divider()
Column(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 4.dp, horizontal = 16.dp)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.heightIn(min = 44.dp),
verticalAlignment = Alignment.CenterVertically
) {
if (item.isMatrixOrg) {
RoundedIconAtom(
size = RoundedIconAtomSize.Medium,
resourceId = R.drawable.ic_matrix,
tint = Color.Unspecified,
)
} else {
RoundedIconAtom(
size = RoundedIconAtomSize.Medium,
imageVector = Icons.Filled.Search,
tint = MaterialTheme.colorScheme.primary,
)
}
Text(
modifier = Modifier
.padding(start = 16.dp)
.weight(1f),
text = item.title,
style = ElementTextStyles.Regular.headline.copy(textAlign = TextAlign.Start),
color = MaterialTheme.colorScheme.primary,
)
if (item.isPublic) {
Icon(
modifier = Modifier
.padding(start = 10.dp)
.size(16.dp),
resourceId = R.drawable.ic_public,
contentDescription = null,
tint = Color.Unspecified,
)
}
}
if (item.subtitle != null) {
Text(
modifier = Modifier
.padding(start = 46.dp, bottom = 12.dp, end = 26.dp),
text = item.subtitle,
style = ElementTextStyles.Regular.subheadline.copy(textAlign = TextAlign.Start),
color = MaterialTheme.colorScheme.secondary,
)
}
}
}
}
@Preview
@Composable
fun AccountProviderViewLightPreview(@PreviewParameter(AccountProviderProvider::class) item: AccountProvider) =
ElementPreviewLight { ContentToPreview(item) }
@Preview
@Composable
fun AccountProviderViewDarkPreview(@PreviewParameter(AccountProviderProvider::class) item: AccountProvider) =
ElementPreviewDark { ContentToPreview(item) }
@Composable
private fun ContentToPreview(item: AccountProvider) {
AccountProviderView(
item = item,
onClick = { }
)
}

View File

@@ -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.changeaccountprovider.form
import io.element.android.libraries.architecture.Async
import kotlinx.coroutines.flow.Flow
/**
* Resolve homeserver base on search terms.
*/
interface HomeserverResolver {
suspend fun resolve(userInput: String): Flow<Async<List<HomeserverData>>>
}

View File

@@ -14,9 +14,9 @@
* limitations under the License.
*/
package io.element.android.features.login.impl.changeaccountprovider.common
package io.element.android.features.login.impl.changeserver
import io.element.android.features.login.impl.accountprovider.item.AccountProvider
import io.element.android.features.login.impl.accountprovider.AccountProvider
sealed interface ChangeServerEvents {
data class ChangeServer(val accountProvider: AccountProvider) : ChangeServerEvents

View File

@@ -14,15 +14,15 @@
* limitations under the License.
*/
package io.element.android.features.login.impl.changeaccountprovider.common
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 io.element.android.features.login.impl.accountprovider.item.AccountProvider
import io.element.android.features.login.impl.datasource.AccountProviderDataSource
import io.element.android.features.login.impl.accountprovider.AccountProvider
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
import io.element.android.features.login.impl.error.ChangeServerError
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.architecture.Presenter

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.features.login.impl.changeaccountprovider.common
package io.element.android.features.login.impl.changeserver
import io.element.android.libraries.architecture.Async

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.features.login.impl.changeaccountprovider.common
package io.element.android.features.login.impl.changeserver
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.architecture.Async

View File

@@ -14,13 +14,13 @@
* limitations under the License.
*/
package io.element.android.features.login.impl.changeaccountprovider.common
package io.element.android.features.login.impl.changeserver
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import io.element.android.features.login.impl.accountprovider.SlidingSyncNotSupportedDialog
import io.element.android.features.login.impl.dialogs.SlidingSyncNotSupportedDialog
import io.element.android.features.login.impl.error.ChangeServerError
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.designsystem.components.ProgressDialog

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.features.login.impl.changeaccountprovider.form
package io.element.android.features.login.impl.changeserver.resolver
data class HomeserverData constructor(
// The computed homeserver url, for which a wellknown file has been retrieved, or just a valid Url

View File

@@ -14,17 +14,15 @@
* limitations under the License.
*/
package io.element.android.features.login.impl.changeaccountprovider.form
package io.element.android.features.login.impl.changeserver.resolver
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.features.login.impl.changeaccountprovider.form.network.WellknownRequest
import io.element.android.features.login.impl.changeserver.resolver.network.WellknownRequest
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.core.bool.orFalse
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.core.data.tryOrNull
import io.element.android.libraries.core.uri.ensureProtocol
import io.element.android.libraries.core.uri.isValidUrl
import io.element.android.libraries.di.AppScope
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.currentCoroutineContext
@@ -38,13 +36,12 @@ import javax.inject.Inject
/**
* Resolve homeserver base on search terms.
*/
@ContributesBinding(AppScope::class)
class DefaultHomeserverResolver @Inject constructor(
class HomeserverResolver @Inject constructor(
private val dispatchers: CoroutineDispatchers,
private val wellknownRequest: WellknownRequest,
) : HomeserverResolver {
) {
override suspend fun resolve(userInput: String): Flow<Async<List<HomeserverData>>> = flow {
suspend fun resolve(userInput: String): Flow<Async<List<HomeserverData>>> = flow {
val flowContext = currentCoroutineContext()
emit(Async.Uninitialized)
// Debounce

View File

@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.login.impl.changeaccountprovider.form.network
package io.element.android.features.login.impl.changeserver.resolver.network
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.di.AppScope

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.features.login.impl.changeaccountprovider.form.network
package io.element.android.features.login.impl.changeserver.resolver.network
import io.element.android.libraries.core.bool.orFalse
import kotlinx.serialization.SerialName

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.features.login.impl.changeaccountprovider.form.network
package io.element.android.features.login.impl.changeserver.resolver.network
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.features.login.impl.changeaccountprovider.form.network
package io.element.android.features.login.impl.changeserver.resolver.network
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.features.login.impl.changeaccountprovider.form.network
package io.element.android.features.login.impl.changeserver.resolver.network
import retrofit2.http.GET

View File

@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.login.impl.changeaccountprovider.form.network
package io.element.android.features.login.impl.changeserver.resolver.network
interface WellknownRequest {
/**

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.features.login.impl.accountprovider
package io.element.android.features.login.impl.dialogs
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.features.login.impl.changeaccountprovider
package io.element.android.features.login.impl.screens.changeaccountprovider
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier

View File

@@ -14,11 +14,11 @@
* limitations under the License.
*/
package io.element.android.features.login.impl.changeaccountprovider
package io.element.android.features.login.impl.screens.changeaccountprovider
import androidx.compose.runtime.Composable
import io.element.android.features.login.impl.accountprovider.item.AccountProvider
import io.element.android.features.login.impl.changeaccountprovider.common.ChangeServerPresenter
import io.element.android.features.login.impl.accountprovider.AccountProvider
import io.element.android.features.login.impl.changeserver.ChangeServerPresenter
import io.element.android.libraries.architecture.Presenter
import javax.inject.Inject

View File

@@ -14,10 +14,10 @@
* limitations under the License.
*/
package io.element.android.features.login.impl.changeaccountprovider
package io.element.android.features.login.impl.screens.changeaccountprovider
import io.element.android.features.login.impl.accountprovider.item.AccountProvider
import io.element.android.features.login.impl.changeaccountprovider.common.ChangeServerState
import io.element.android.features.login.impl.accountprovider.AccountProvider
import io.element.android.features.login.impl.changeserver.ChangeServerState
// Do not use default value, so no member get forgotten in the presenters.
data class ChangeAccountProviderState constructor(

View File

@@ -14,11 +14,11 @@
* limitations under the License.
*/
package io.element.android.features.login.impl.changeaccountprovider
package io.element.android.features.login.impl.screens.changeaccountprovider
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.login.impl.accountprovider.item.anAccountProvider
import io.element.android.features.login.impl.changeaccountprovider.common.aChangeServerState
import io.element.android.features.login.impl.accountprovider.anAccountProvider
import io.element.android.features.login.impl.changeserver.aChangeServerState
open class ChangeAccountProviderStateProvider : PreviewParameterProvider<ChangeAccountProviderState> {
override val values: Sequence<ChangeAccountProviderState>

View File

@@ -16,7 +16,7 @@
@file:OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
package io.element.android.features.login.impl.changeaccountprovider
package io.element.android.features.login.impl.screens.changeaccountprovider
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -40,10 +40,10 @@ 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.accountprovider.item.AccountProvider
import io.element.android.features.login.impl.accountprovider.item.AccountProviderView
import io.element.android.features.login.impl.changeaccountprovider.common.ChangeServerEvents
import io.element.android.features.login.impl.changeaccountprovider.common.ChangeServerView
import io.element.android.features.login.impl.accountprovider.AccountProvider
import io.element.android.features.login.impl.accountprovider.AccountProviderView
import io.element.android.features.login.impl.changeserver.ChangeServerEvents
import io.element.android.features.login.impl.changeserver.ChangeServerView
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.preview.ElementPreviewDark

View File

@@ -14,9 +14,9 @@
* limitations under the License.
*/
package io.element.android.features.login.impl.accountprovider
package io.element.android.features.login.impl.screens.confirmaccountprovider
sealed interface AccountProviderEvents {
object Continue : AccountProviderEvents
object ClearError : AccountProviderEvents
sealed interface ConfirmAccountProviderEvents {
object Continue : ConfirmAccountProviderEvents
object ClearError : ConfirmAccountProviderEvents
}

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.features.login.impl.accountprovider
package io.element.android.features.login.impl.screens.confirmaccountprovider
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@@ -33,10 +33,10 @@ import io.element.android.libraries.di.AppScope
import io.element.android.libraries.matrix.api.auth.OidcDetails
@ContributesNode(AppScope::class)
class AccountProviderNode @AssistedInject constructor(
class ConfirmAccountProviderNode @AssistedInject constructor(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
presenterFactory: AccountProviderPresenter.Factory,
presenterFactory: ConfirmAccountProviderPresenter.Factory,
) : Node(buildContext, plugins = plugins) {
data class Inputs(
@@ -45,7 +45,7 @@ class AccountProviderNode @AssistedInject constructor(
private val inputs: Inputs = inputs()
private val presenter = presenterFactory.create(
AccountProviderPresenterParams(
ConfirmAccountProviderPresenter.Params(
isAccountCreation = inputs.isAccountCreation,
)
)
@@ -72,7 +72,7 @@ class AccountProviderNode @AssistedInject constructor(
override fun View(modifier: Modifier) {
val state = presenter.present()
val context = LocalContext.current
AccountProviderView(
ConfirmAccountProviderView(
state = state,
modifier = modifier,
onOidcDetails = ::onOidcDetails,

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.features.login.impl.accountprovider
package io.element.android.features.login.impl.screens.confirmaccountprovider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
@@ -26,7 +26,7 @@ import androidx.compose.runtime.rememberCoroutineScope
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import io.element.android.features.login.impl.datasource.AccountProviderDataSource
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
import io.element.android.features.login.impl.error.ChangeServerError
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.architecture.Presenter
@@ -37,23 +37,23 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import java.net.URL
data class AccountProviderPresenterParams(
val isAccountCreation: Boolean,
)
class AccountProviderPresenter @AssistedInject constructor(
@Assisted private val params: AccountProviderPresenterParams,
class ConfirmAccountProviderPresenter @AssistedInject constructor(
@Assisted private val params: Params,
private val accountProviderDataSource: AccountProviderDataSource,
private val authenticationService: MatrixAuthenticationService
) : Presenter<AccountProviderState> {
) : Presenter<ConfirmAccountProviderState> {
data class Params(
val isAccountCreation: Boolean,
)
@AssistedFactory
interface Factory {
fun create(params: AccountProviderPresenterParams): AccountProviderPresenter
fun create(params: Params): ConfirmAccountProviderPresenter
}
@Composable
override fun present(): AccountProviderState {
override fun present(): ConfirmAccountProviderState {
val accountProvider by accountProviderDataSource.flow().collectAsState()
val localCoroutineScope = rememberCoroutineScope()
@@ -61,16 +61,16 @@ class AccountProviderPresenter @AssistedInject constructor(
mutableStateOf(Async.Uninitialized)
}
fun handleEvents(event: AccountProviderEvents) {
fun handleEvents(event: ConfirmAccountProviderEvents) {
when (event) {
AccountProviderEvents.Continue -> {
ConfirmAccountProviderEvents.Continue -> {
localCoroutineScope.submit(accountProvider.title, loginFlowAction)
}
AccountProviderEvents.ClearError -> loginFlowAction.value = Async.Uninitialized
ConfirmAccountProviderEvents.ClearError -> loginFlowAction.value = Async.Uninitialized
}
}
return AccountProviderState(
return ConfirmAccountProviderState(
accountProvider = accountProvider,
isAccountCreation = params.isAccountCreation,
loginFlow = loginFlowAction.value,

View File

@@ -14,18 +14,18 @@
* limitations under the License.
*/
package io.element.android.features.login.impl.accountprovider
package io.element.android.features.login.impl.screens.confirmaccountprovider
import io.element.android.features.login.impl.accountprovider.item.AccountProvider
import io.element.android.features.login.impl.accountprovider.AccountProvider
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.matrix.api.auth.OidcDetails
// Do not use default value, so no member get forgotten in the presenters.
data class AccountProviderState(
data class ConfirmAccountProviderState(
val accountProvider: AccountProvider,
val isAccountCreation: Boolean,
val loginFlow: Async<LoginFlow>,
val eventSink: (AccountProviderEvents) -> Unit
val eventSink: (ConfirmAccountProviderEvents) -> Unit
) {
val submitEnabled: Boolean get() = accountProvider.title.isNotEmpty() && (loginFlow is Async.Uninitialized || loginFlow is Async.Loading)
}

View File

@@ -14,21 +14,21 @@
* limitations under the License.
*/
package io.element.android.features.login.impl.accountprovider
package io.element.android.features.login.impl.screens.confirmaccountprovider
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.login.impl.accountprovider.item.anAccountProvider
import io.element.android.features.login.impl.accountprovider.anAccountProvider
import io.element.android.libraries.architecture.Async
open class AccountProviderStateProvider : PreviewParameterProvider<AccountProviderState> {
override val values: Sequence<AccountProviderState>
open class ConfirmAccountProviderStateProvider : PreviewParameterProvider<ConfirmAccountProviderState> {
override val values: Sequence<ConfirmAccountProviderState>
get() = sequenceOf(
anAccountProviderState(),
aConfirmAccountProviderState(),
// Add other state here
)
}
fun anAccountProviderState() = AccountProviderState(
fun aConfirmAccountProviderState() = ConfirmAccountProviderState(
accountProvider = anAccountProvider(),
isAccountCreation = false,
loginFlow = Async.Uninitialized,

View File

@@ -0,0 +1,162 @@
/*
* 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.screens.confirmaccountprovider
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AccountCircle
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
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.dialogs.SlidingSyncNotSupportedDialog
import io.element.android.features.login.impl.error.ChangeServerError
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.button.ButtonWithProgress
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.Text
import io.element.android.libraries.designsystem.theme.components.TextButton
import io.element.android.libraries.matrix.api.auth.OidcDetails
import io.element.android.libraries.testtags.TestTags
import io.element.android.libraries.testtags.testTag
@Composable
fun ConfirmAccountProviderView(
state: ConfirmAccountProviderState,
modifier: Modifier = Modifier,
onOidcDetails: (OidcDetails) -> Unit = {},
onLoginPasswordNeeded: () -> Unit = {},
onLearnMoreClicked: () -> Unit = {},
onChange: () -> Unit = {},
) {
val isLoading by remember(state.loginFlow) {
derivedStateOf {
state.loginFlow is Async.Loading
}
}
val eventSink = state.eventSink
HeaderFooterPage(
modifier = modifier,
header = {
IconTitleSubtitleMolecule(
modifier = Modifier.padding(top = 60.dp),
iconImageVector = Icons.Filled.AccountCircle,
title = stringResource(
id = if (state.isAccountCreation) {
R.string.screen_account_provider_signup_title
} else {
R.string.screen_account_provider_signin_title
},
state.accountProvider.title
),
subTitle = stringResource(
id = if (state.isAccountCreation) {
R.string.screen_account_provider_signup_subtitle
} else {
// Use same value for now.
R.string.screen_account_provider_signup_subtitle
},
)
)
},
footer = {
ButtonColumnMolecule {
ButtonWithProgress(
text = stringResource(id = R.string.screen_account_provider_continue),
showProgress = isLoading,
onClick = { eventSink.invoke(ConfirmAccountProviderEvents.Continue) },
enabled = state.submitEnabled,
modifier = Modifier
.fillMaxWidth()
.testTag(TestTags.loginContinue)
)
TextButton(
onClick = {
onChange()
},
enabled = true,
modifier = Modifier
.fillMaxWidth()
.testTag(TestTags.loginChangeServer)
) {
Text(text = stringResource(id = R.string.screen_account_provider_change))
}
}
}
) {
when (state.loginFlow) {
is Async.Failure -> {
when (val error = state.loginFlow.error) {
is ChangeServerError.InlineErrorMessage -> {
ErrorDialog(
content = error.message(),
onDismiss = {
eventSink.invoke(ConfirmAccountProviderEvents.ClearError)
}
)
}
is ChangeServerError.SlidingSyncAlert -> {
SlidingSyncNotSupportedDialog(onLearnMoreClicked = {
onLearnMoreClicked()
eventSink(ConfirmAccountProviderEvents.ClearError)
}, onDismiss = {
eventSink(ConfirmAccountProviderEvents.ClearError)
})
}
}
}
is Async.Loading -> Unit // The Continue button shows the loading state
is Async.Success -> {
when (val loginFlowState = state.loginFlow.state) {
is LoginFlow.OidcFlow -> onOidcDetails(loginFlowState.oidcDetails)
LoginFlow.PasswordLogin -> onLoginPasswordNeeded()
}
}
Async.Uninitialized -> Unit
}
}
}
@Preview
@Composable
fun ConfirmAccountProviderViewLightPreview(@PreviewParameter(ConfirmAccountProviderStateProvider::class) state: ConfirmAccountProviderState) =
ElementPreviewLight { ContentToPreview(state) }
@Preview
@Composable
fun ConfirmAccountProviderViewDarkPreview(@PreviewParameter(ConfirmAccountProviderStateProvider::class) state: ConfirmAccountProviderState) =
ElementPreviewDark { ContentToPreview(state) }
@Composable
private fun ContentToPreview(state: ConfirmAccountProviderState) {
ConfirmAccountProviderView(
state = state,
)
}

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.features.login.impl.loginpassword
package io.element.android.features.login.impl.screens.loginpassword
sealed interface LoginPasswordEvents {
data class SetLogin(val login: String) : LoginPasswordEvents

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.features.login.impl.loginpassword
package io.element.android.features.login.impl.screens.loginpassword
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.features.login.impl.loginpassword
package io.element.android.features.login.impl.screens.loginpassword
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
@@ -24,7 +24,7 @@ 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.datasource.AccountProviderDataSource
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService

View File

@@ -14,10 +14,10 @@
* limitations under the License.
*/
package io.element.android.features.login.impl.loginpassword
package io.element.android.features.login.impl.screens.loginpassword
import android.os.Parcelable
import io.element.android.features.login.impl.accountprovider.item.AccountProvider
import io.element.android.features.login.impl.accountprovider.AccountProvider
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.matrix.api.core.SessionId
import kotlinx.parcelize.Parcelize

View File

@@ -14,10 +14,10 @@
* limitations under the License.
*/
package io.element.android.features.login.impl.loginpassword
package io.element.android.features.login.impl.screens.loginpassword
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.login.impl.accountprovider.item.anAccountProvider
import io.element.android.features.login.impl.accountprovider.anAccountProvider
import io.element.android.libraries.architecture.Async
open class LoginPasswordStateProvider : PreviewParameterProvider<LoginPasswordState> {

View File

@@ -1,5 +1,5 @@
/*
* Copyright (c) 2022 New Vector Ltd
* 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.
@@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.features.login.impl.loginpassword
package io.element.android.features.login.impl.screens.loginpassword
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column

View File

@@ -14,11 +14,11 @@
* limitations under the License.
*/
package io.element.android.features.login.impl.changeaccountprovider.form
package io.element.android.features.login.impl.screens.searchaccountprovider
sealed interface ChangeAccountProviderFormEvents {
sealed interface SearchAccountProviderEvents {
/**
* The user has typed something, expect to get a list of result in the state.
*/
data class UserInput(val input: String) : ChangeAccountProviderFormEvents
data class UserInput(val input: String) : SearchAccountProviderEvents
}

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.features.login.impl.changeaccountprovider.form
package io.element.android.features.login.impl.screens.searchaccountprovider
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@@ -30,10 +30,10 @@ import io.element.android.features.login.impl.util.openLearnMorePage
import io.element.android.libraries.di.AppScope
@ContributesNode(AppScope::class)
class ChangeAccountProviderFormNode @AssistedInject constructor(
class SearchAccountProviderNode @AssistedInject constructor(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
private val presenter: ChangeAccountProviderFormPresenter,
private val presenter: SearchAccountProviderPresenter,
) : Node(buildContext, plugins = plugins) {
interface Callback : Plugin {
@@ -48,7 +48,7 @@ class ChangeAccountProviderFormNode @AssistedInject constructor(
override fun View(modifier: Modifier) {
val state = presenter.present()
val context = LocalContext.current
ChangeAccountProviderFormView(
SearchAccountProviderView(
state = state,
modifier = modifier,
onBackPressed = ::navigateUp,

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.features.login.impl.changeaccountprovider.form
package io.element.android.features.login.impl.screens.searchaccountprovider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@@ -23,18 +23,20 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import io.element.android.features.login.impl.changeaccountprovider.common.ChangeServerPresenter
import io.element.android.features.login.impl.changeserver.ChangeServerPresenter
import io.element.android.features.login.impl.changeserver.resolver.HomeserverData
import io.element.android.features.login.impl.changeserver.resolver.HomeserverResolver
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.architecture.Presenter
import javax.inject.Inject
class ChangeAccountProviderFormPresenter @Inject constructor(
class SearchAccountProviderPresenter @Inject constructor(
private val homeserverResolver: HomeserverResolver,
private val changeServerPresenter: ChangeServerPresenter,
) : Presenter<ChangeAccountProviderFormState> {
) : Presenter<SearchAccountProviderState> {
@Composable
override fun present(): ChangeAccountProviderFormState {
override fun present(): SearchAccountProviderState {
var userInput by rememberSaveable {
mutableStateOf("")
}
@@ -50,15 +52,15 @@ class ChangeAccountProviderFormPresenter @Inject constructor(
}
}
fun handleEvents(event: ChangeAccountProviderFormEvents) {
fun handleEvents(event: SearchAccountProviderEvents) {
when (event) {
is ChangeAccountProviderFormEvents.UserInput -> {
is SearchAccountProviderEvents.UserInput -> {
userInput = event.input
}
}
}
return ChangeAccountProviderFormState(
return SearchAccountProviderState(
userInput = userInput,
userInputResult = data,
changeServerState = changeServerState,

View File

@@ -14,15 +14,16 @@
* limitations under the License.
*/
package io.element.android.features.login.impl.changeaccountprovider.form
package io.element.android.features.login.impl.screens.searchaccountprovider
import io.element.android.features.login.impl.changeaccountprovider.common.ChangeServerState
import io.element.android.features.login.impl.changeserver.ChangeServerState
import io.element.android.features.login.impl.changeserver.resolver.HomeserverData
import io.element.android.libraries.architecture.Async
// Do not use default value, so no member get forgotten in the presenters.
data class ChangeAccountProviderFormState(
data class SearchAccountProviderState(
val userInput: String,
val userInputResult: Async<List<HomeserverData>>,
val changeServerState: ChangeServerState,
val eventSink: (ChangeAccountProviderFormEvents) -> Unit
val eventSink: (SearchAccountProviderEvents) -> Unit
)

View File

@@ -14,25 +14,26 @@
* limitations under the License.
*/
package io.element.android.features.login.impl.changeaccountprovider.form
package io.element.android.features.login.impl.screens.searchaccountprovider
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.login.impl.changeaccountprovider.common.aChangeServerState
import io.element.android.features.login.impl.changeserver.aChangeServerState
import io.element.android.features.login.impl.changeserver.resolver.HomeserverData
import io.element.android.libraries.architecture.Async
open class ChangeAccountProviderFormStateProvider : PreviewParameterProvider<ChangeAccountProviderFormState> {
override val values: Sequence<ChangeAccountProviderFormState>
open class SearchAccountProviderStateProvider : PreviewParameterProvider<SearchAccountProviderState> {
override val values: Sequence<SearchAccountProviderState>
get() = sequenceOf(
aChangeAccountProviderFormState(),
aChangeAccountProviderFormState(userInputResult = Async.Success(aHomeserverDataList())),
aSearchAccountProviderState(),
aSearchAccountProviderState(userInputResult = Async.Success(aHomeserverDataList())),
// Add other state here
)
}
fun aChangeAccountProviderFormState(
fun aSearchAccountProviderState(
userInput: String = "",
userInputResult: Async<List<HomeserverData>> = Async.Uninitialized,
) = ChangeAccountProviderFormState(
) = SearchAccountProviderState(
userInput = userInput,
userInputResult = userInputResult,
changeServerState = aChangeServerState(),

View File

@@ -16,7 +16,7 @@
@file:OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
package io.element.android.features.login.impl.changeaccountprovider.form
package io.element.android.features.login.impl.screens.searchaccountprovider
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -50,10 +50,11 @@ 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.accountprovider.item.AccountProvider
import io.element.android.features.login.impl.accountprovider.item.AccountProviderView
import io.element.android.features.login.impl.changeaccountprovider.common.ChangeServerEvents
import io.element.android.features.login.impl.changeaccountprovider.common.ChangeServerView
import io.element.android.features.login.impl.accountprovider.AccountProvider
import io.element.android.features.login.impl.accountprovider.AccountProviderView
import io.element.android.features.login.impl.changeserver.ChangeServerEvents
import io.element.android.features.login.impl.changeserver.ChangeServerView
import io.element.android.features.login.impl.changeserver.resolver.HomeserverData
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
import io.element.android.libraries.designsystem.components.button.BackButton
@@ -72,8 +73,8 @@ import io.element.android.libraries.testtags.testTag
* https://www.figma.com/file/o9p34zmiuEpZRyvZXJZAYL/FTUE?type=design&node-id=611-61435
*/
@Composable
fun ChangeAccountProviderFormView(
state: ChangeAccountProviderFormState,
fun SearchAccountProviderView(
state: SearchAccountProviderState,
modifier: Modifier = Modifier,
onBackPressed: () -> Unit = {},
onLearnMoreClicked: () -> Unit = {},
@@ -123,7 +124,7 @@ fun ChangeAccountProviderFormView(
.testTag(TestTags.changeServerServer),
onValueChange = {
userInputState = it
eventSink(ChangeAccountProviderFormEvents.UserInput(it))
eventSink(SearchAccountProviderEvents.UserInput(it))
},
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Uri,
@@ -135,7 +136,7 @@ fun ChangeAccountProviderFormView(
{
IconButton(onClick = {
userInputState = ""
eventSink(ChangeAccountProviderFormEvents.UserInput(""))
eventSink(SearchAccountProviderEvents.UserInput(""))
}) {
Icon(
imageVector = Icons.Filled.Close,
@@ -202,17 +203,17 @@ private fun HomeserverData.toAccountProvider(): AccountProvider {
@Preview
@Composable
fun ChangeAccountProviderFormViewLightPreview(@PreviewParameter(ChangeAccountProviderFormStateProvider::class) state: ChangeAccountProviderFormState) =
fun SearchAccountProviderViewLightPreview(@PreviewParameter(SearchAccountProviderStateProvider::class) state: SearchAccountProviderState) =
ElementPreviewLight { ContentToPreview(state) }
@Preview
@Composable
fun ChangeAccountProviderFormViewDarkPreview(@PreviewParameter(ChangeAccountProviderFormStateProvider::class) state: ChangeAccountProviderFormState) =
fun SearchAccountProviderViewDarkPreview(@PreviewParameter(SearchAccountProviderStateProvider::class) state: SearchAccountProviderState) =
ElementPreviewDark { ContentToPreview(state) }
@Composable
private fun ContentToPreview(state: ChangeAccountProviderFormState) {
ChangeAccountProviderFormView(
private fun ContentToPreview(state: SearchAccountProviderState) {
SearchAccountProviderView(
state = state,
)
}

View File

@@ -16,7 +16,7 @@
package io.element.android.features.login.impl.util
import io.element.android.features.login.impl.accountprovider.item.AccountProvider
import io.element.android.features.login.impl.accountprovider.AccountProvider
object LoginConstants {
const val MATRIX_ORG_URL = "matrix.org"

View File

@@ -14,14 +14,14 @@
* limitations under the License.
*/
package io.element.android.features.login.impl.changeaccountprovider.common
package io.element.android.features.login.impl.changeserver
import app.cash.molecule.RecompositionClock
import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.features.login.impl.accountprovider.item.AccountProvider
import io.element.android.features.login.impl.datasource.AccountProviderDataSource
import io.element.android.features.login.impl.accountprovider.AccountProvider
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.matrix.test.A_HOMESERVER
import io.element.android.libraries.matrix.test.A_HOMESERVER_URL

View File

@@ -14,10 +14,7 @@
* limitations under the License.
*/
package io.element.android.features.login.impl.changeaccountprovider.form
import io.element.android.features.login.impl.changeaccountprovider.form.network.WellKnown
import io.element.android.features.login.impl.changeaccountprovider.form.network.WellknownRequest
package io.element.android.features.login.impl.changeserver.resolver.network
class FakeWellknownRequest : WellknownRequest {
private var resultMap: Map<String, WellKnown> = emptyMap()

View File

@@ -14,15 +14,15 @@
* limitations under the License.
*/
package io.element.android.features.login.impl.changeaccountprovider
package io.element.android.features.login.impl.screens.changeaccountprovider
import app.cash.molecule.RecompositionClock
import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.features.login.impl.accountprovider.item.AccountProvider
import io.element.android.features.login.impl.changeaccountprovider.common.ChangeServerPresenter
import io.element.android.features.login.impl.datasource.AccountProviderDataSource
import io.element.android.features.login.impl.accountprovider.AccountProvider
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
import io.element.android.features.login.impl.changeserver.ChangeServerPresenter
import io.element.android.libraries.matrix.test.auth.FakeAuthenticationService
import kotlinx.coroutines.test.runTest
import org.junit.Test

View File

@@ -14,13 +14,13 @@
* limitations under the License.
*/
package io.element.android.features.login.impl.accountprovider
package io.element.android.features.login.impl.screens.confirmaccountprovider
import app.cash.molecule.RecompositionClock
import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.features.login.impl.datasource.AccountProviderDataSource
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
import io.element.android.features.login.impl.util.defaultAccountProvider
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.matrix.test.A_HOMESERVER
@@ -30,11 +30,11 @@ import io.element.android.libraries.matrix.test.auth.FakeAuthenticationService
import kotlinx.coroutines.test.runTest
import org.junit.Test
class AccountProviderPresenterTest {
class ConfirmAccountProviderPresenterTest {
@Test
fun `present - initial test`() = runTest {
val presenter = AccountProviderPresenter(
AccountProviderPresenterParams(isAccountCreation = false),
val presenter = ConfirmAccountProviderPresenter(
ConfirmAccountProviderPresenter.Params(isAccountCreation = false),
AccountProviderDataSource(),
FakeAuthenticationService(),
)
@@ -52,8 +52,8 @@ class AccountProviderPresenterTest {
@Test
fun `present - continue password login`() = runTest {
val authServer = FakeAuthenticationService()
val presenter = AccountProviderPresenter(
AccountProviderPresenterParams(isAccountCreation = false),
val presenter = ConfirmAccountProviderPresenter(
ConfirmAccountProviderPresenter.Params(isAccountCreation = false),
AccountProviderDataSource(),
authServer,
)
@@ -62,7 +62,7 @@ class AccountProviderPresenterTest {
presenter.present()
}.test {
val initialState = awaitItem()
initialState.eventSink.invoke(AccountProviderEvents.Continue)
initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue)
val loadingState = awaitItem()
assertThat(loadingState.submitEnabled).isTrue()
assertThat(loadingState.loginFlow).isInstanceOf(Async.Loading::class.java)
@@ -76,8 +76,8 @@ class AccountProviderPresenterTest {
@Test
fun `present - continue oidc`() = runTest {
val authServer = FakeAuthenticationService()
val presenter = AccountProviderPresenter(
AccountProviderPresenterParams(isAccountCreation = false),
val presenter = ConfirmAccountProviderPresenter(
ConfirmAccountProviderPresenter.Params(isAccountCreation = false),
AccountProviderDataSource(),
authServer,
)
@@ -86,7 +86,7 @@ class AccountProviderPresenterTest {
presenter.present()
}.test {
val initialState = awaitItem()
initialState.eventSink.invoke(AccountProviderEvents.Continue)
initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue)
val loadingState = awaitItem()
assertThat(loadingState.submitEnabled).isTrue()
assertThat(loadingState.loginFlow).isInstanceOf(Async.Loading::class.java)
@@ -100,8 +100,8 @@ class AccountProviderPresenterTest {
@Test
fun `present - submit fails`() = runTest {
val authServer = FakeAuthenticationService()
val presenter = AccountProviderPresenter(
AccountProviderPresenterParams(isAccountCreation = false),
val presenter = ConfirmAccountProviderPresenter(
ConfirmAccountProviderPresenter.Params(isAccountCreation = false),
AccountProviderDataSource(),
authServer,
)
@@ -110,7 +110,7 @@ class AccountProviderPresenterTest {
}.test {
val initialState = awaitItem()
authServer.givenChangeServerError(Throwable())
initialState.eventSink.invoke(AccountProviderEvents.Continue)
initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue)
skipItems(1) // Loading
val failureState = awaitItem()
assertThat(failureState.submitEnabled).isFalse()
@@ -121,8 +121,8 @@ class AccountProviderPresenterTest {
@Test
fun `present - clear error`() = runTest {
val authenticationService = FakeAuthenticationService()
val presenter = AccountProviderPresenter(
AccountProviderPresenterParams(isAccountCreation = false),
val presenter = ConfirmAccountProviderPresenter(
ConfirmAccountProviderPresenter.Params(isAccountCreation = false),
AccountProviderDataSource(),
authenticationService,
)
@@ -133,7 +133,7 @@ class AccountProviderPresenterTest {
// Submit will return an error
authenticationService.givenChangeServerError(A_THROWABLE)
initialState.eventSink(AccountProviderEvents.Continue)
initialState.eventSink(ConfirmAccountProviderEvents.Continue)
skipItems(1) // Loading
@@ -142,7 +142,7 @@ class AccountProviderPresenterTest {
assertThat(submittedState.loginFlow).isInstanceOf(Async.Failure::class.java)
// Assert the error is then cleared
submittedState.eventSink(AccountProviderEvents.ClearError)
submittedState.eventSink(ConfirmAccountProviderEvents.ClearError)
val clearedState = awaitItem()
assertThat(clearedState.loginFlow).isEqualTo(Async.Uninitialized)
}

View File

@@ -14,13 +14,13 @@
* limitations under the License.
*/
package io.element.android.features.login.impl.loginpassword
package io.element.android.features.login.impl.screens.loginpassword
import app.cash.molecule.RecompositionClock
import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.features.login.impl.datasource.AccountProviderDataSource
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
import io.element.android.features.login.impl.util.defaultAccountProvider
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.matrix.api.core.SessionId

View File

@@ -14,17 +14,19 @@
* limitations under the License.
*/
package io.element.android.features.login.impl.changeaccountprovider.form
package io.element.android.features.login.impl.screens.searchaccountprovider
import app.cash.molecule.RecompositionClock
import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.features.login.impl.changeaccountprovider.common.ChangeServerPresenter
import io.element.android.features.login.impl.changeaccountprovider.form.network.WellKnown
import io.element.android.features.login.impl.changeaccountprovider.form.network.WellKnownBaseConfig
import io.element.android.features.login.impl.changeaccountprovider.form.network.WellKnownSlidingSyncConfig
import io.element.android.features.login.impl.datasource.AccountProviderDataSource
import io.element.android.features.login.impl.changeserver.ChangeServerPresenter
import io.element.android.features.login.impl.changeserver.resolver.network.WellKnown
import io.element.android.features.login.impl.changeserver.resolver.network.WellKnownBaseConfig
import io.element.android.features.login.impl.changeserver.resolver.network.WellKnownSlidingSyncConfig
import io.element.android.features.login.impl.changeserver.resolver.HomeserverResolver
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
import io.element.android.features.login.impl.changeserver.resolver.network.FakeWellknownRequest
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.matrix.test.A_HOMESERVER_URL
import io.element.android.libraries.matrix.test.auth.FakeAuthenticationService
@@ -32,7 +34,7 @@ import io.element.android.tests.testutils.testCoroutineDispatchers
import kotlinx.coroutines.test.runTest
import org.junit.Test
class ChangeAccountProviderFormPresenterTest {
class SearchAccountProviderPresenterTest {
@Test
fun `present - initial state`() = runTest {
val fakeWellknownRequest = FakeWellknownRequest()
@@ -40,8 +42,8 @@ class ChangeAccountProviderFormPresenterTest {
FakeAuthenticationService(),
AccountProviderDataSource()
)
val presenter = ChangeAccountProviderFormPresenter(
DefaultHomeserverResolver(testCoroutineDispatchers(), fakeWellknownRequest),
val presenter = SearchAccountProviderPresenter(
HomeserverResolver(testCoroutineDispatchers(), fakeWellknownRequest),
changeServerPresenter
)
moleculeFlow(RecompositionClock.Immediate) {
@@ -60,15 +62,15 @@ class ChangeAccountProviderFormPresenterTest {
FakeAuthenticationService(),
AccountProviderDataSource()
)
val presenter = ChangeAccountProviderFormPresenter(
DefaultHomeserverResolver(testCoroutineDispatchers(), fakeWellknownRequest),
val presenter = SearchAccountProviderPresenter(
HomeserverResolver(testCoroutineDispatchers(), fakeWellknownRequest),
changeServerPresenter
)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
initialState.eventSink.invoke(ChangeAccountProviderFormEvents.UserInput("test"))
initialState.eventSink.invoke(SearchAccountProviderEvents.UserInput("test"))
val withInputState = awaitItem()
assertThat(withInputState.userInput).isEqualTo("test")
assertThat(initialState.userInputResult).isEqualTo(Async.Uninitialized)
@@ -84,15 +86,15 @@ class ChangeAccountProviderFormPresenterTest {
FakeAuthenticationService(),
AccountProviderDataSource()
)
val presenter = ChangeAccountProviderFormPresenter(
DefaultHomeserverResolver(testCoroutineDispatchers(), fakeWellknownRequest),
val presenter = SearchAccountProviderPresenter(
HomeserverResolver(testCoroutineDispatchers(), fakeWellknownRequest),
changeServerPresenter
)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
initialState.eventSink.invoke(ChangeAccountProviderFormEvents.UserInput("https://test.org"))
initialState.eventSink.invoke(SearchAccountProviderEvents.UserInput("https://test.org"))
val withInputState = awaitItem()
assertThat(withInputState.userInput).isEqualTo("https://test.org")
assertThat(initialState.userInputResult).isEqualTo(Async.Uninitialized)
@@ -119,15 +121,15 @@ class ChangeAccountProviderFormPresenterTest {
FakeAuthenticationService(),
AccountProviderDataSource()
)
val presenter = ChangeAccountProviderFormPresenter(
DefaultHomeserverResolver(testCoroutineDispatchers(), fakeWellknownRequest),
val presenter = SearchAccountProviderPresenter(
HomeserverResolver(testCoroutineDispatchers(), fakeWellknownRequest),
changeServerPresenter
)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
initialState.eventSink.invoke(ChangeAccountProviderFormEvents.UserInput("test"))
initialState.eventSink.invoke(SearchAccountProviderEvents.UserInput("test"))
val withInputState = awaitItem()
assertThat(withInputState.userInput).isEqualTo("test")
assertThat(initialState.userInputResult).isEqualTo(Async.Uninitialized)
@@ -154,15 +156,15 @@ class ChangeAccountProviderFormPresenterTest {
FakeAuthenticationService(),
AccountProviderDataSource()
)
val presenter = ChangeAccountProviderFormPresenter(
DefaultHomeserverResolver(testCoroutineDispatchers(), fakeWellknownRequest),
val presenter = SearchAccountProviderPresenter(
HomeserverResolver(testCoroutineDispatchers(), fakeWellknownRequest),
changeServerPresenter
)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
initialState.eventSink.invoke(ChangeAccountProviderFormEvents.UserInput("test"))
initialState.eventSink.invoke(SearchAccountProviderEvents.UserInput("test"))
val withInputState = awaitItem()
assertThat(withInputState.userInput).isEqualTo("test")
assertThat(initialState.userInputResult).isEqualTo(Async.Uninitialized)

View File

@@ -20,9 +20,9 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import io.element.android.features.login.impl.datasource.AccountProviderDataSource
import io.element.android.features.login.impl.loginpassword.LoginPasswordPresenter
import io.element.android.features.login.impl.loginpassword.LoginPasswordView
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
import io.element.android.features.login.impl.screens.loginpassword.LoginPasswordPresenter
import io.element.android.features.login.impl.screens.loginpassword.LoginPasswordView
import io.element.android.features.login.impl.util.defaultAccountProvider
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService