Remove Wait list screen #3293
This commit is contained in:
parent
aa40c3618b
commit
39c296efc6
27 changed files with 4 additions and 570 deletions
|
|
@ -19,7 +19,6 @@ import com.bumble.appyx.core.modality.BuildContext
|
||||||
import com.bumble.appyx.core.node.Node
|
import com.bumble.appyx.core.node.Node
|
||||||
import com.bumble.appyx.core.plugin.Plugin
|
import com.bumble.appyx.core.plugin.Plugin
|
||||||
import com.bumble.appyx.navmodel.backstack.BackStack
|
import com.bumble.appyx.navmodel.backstack.BackStack
|
||||||
import com.bumble.appyx.navmodel.backstack.operation.newRoot
|
|
||||||
import com.bumble.appyx.navmodel.backstack.operation.push
|
import com.bumble.appyx.navmodel.backstack.operation.push
|
||||||
import com.bumble.appyx.navmodel.backstack.operation.singleTop
|
import com.bumble.appyx.navmodel.backstack.operation.singleTop
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
|
|
@ -31,10 +30,8 @@ import io.element.android.features.login.impl.accountprovider.AccountProviderDat
|
||||||
import io.element.android.features.login.impl.qrcode.QrCodeLoginFlowNode
|
import io.element.android.features.login.impl.qrcode.QrCodeLoginFlowNode
|
||||||
import io.element.android.features.login.impl.screens.changeaccountprovider.ChangeAccountProviderNode
|
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.confirmaccountprovider.ConfirmAccountProviderNode
|
||||||
import io.element.android.features.login.impl.screens.loginpassword.LoginFormState
|
|
||||||
import io.element.android.features.login.impl.screens.loginpassword.LoginPasswordNode
|
import io.element.android.features.login.impl.screens.loginpassword.LoginPasswordNode
|
||||||
import io.element.android.features.login.impl.screens.searchaccountprovider.SearchAccountProviderNode
|
import io.element.android.features.login.impl.screens.searchaccountprovider.SearchAccountProviderNode
|
||||||
import io.element.android.features.login.impl.screens.waitlistscreen.WaitListNode
|
|
||||||
import io.element.android.libraries.architecture.BackstackView
|
import io.element.android.libraries.architecture.BackstackView
|
||||||
import io.element.android.libraries.architecture.BaseFlowNode
|
import io.element.android.libraries.architecture.BaseFlowNode
|
||||||
import io.element.android.libraries.architecture.NodeInputs
|
import io.element.android.libraries.architecture.NodeInputs
|
||||||
|
|
@ -112,9 +109,6 @@ class LoginFlowNode @AssistedInject constructor(
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data object LoginPassword : NavTarget
|
data object LoginPassword : NavTarget
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class WaitList(val loginFormState: LoginFormState) : NavTarget
|
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class OidcView(val oidcDetails: OidcDetails) : NavTarget
|
data class OidcView(val oidcDetails: OidcDetails) : NavTarget
|
||||||
}
|
}
|
||||||
|
|
@ -181,27 +175,11 @@ class LoginFlowNode @AssistedInject constructor(
|
||||||
createNode<SearchAccountProviderNode>(buildContext, plugins = listOf(callback))
|
createNode<SearchAccountProviderNode>(buildContext, plugins = listOf(callback))
|
||||||
}
|
}
|
||||||
NavTarget.LoginPassword -> {
|
NavTarget.LoginPassword -> {
|
||||||
val callback = object : LoginPasswordNode.Callback {
|
createNode<LoginPasswordNode>(buildContext)
|
||||||
override fun onWaitListError(loginFormState: LoginFormState) {
|
|
||||||
backstack.newRoot(NavTarget.WaitList(loginFormState))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
createNode<LoginPasswordNode>(buildContext, plugins = listOf(callback))
|
|
||||||
}
|
}
|
||||||
is NavTarget.OidcView -> {
|
is NavTarget.OidcView -> {
|
||||||
oidcEntryPoint.createFallbackWebViewNode(this, buildContext, navTarget.oidcDetails.url)
|
oidcEntryPoint.createFallbackWebViewNode(this, buildContext, navTarget.oidcDetails.url)
|
||||||
}
|
}
|
||||||
is NavTarget.WaitList -> {
|
|
||||||
val inputs = WaitListNode.Inputs(
|
|
||||||
loginFormState = navTarget.loginFormState,
|
|
||||||
)
|
|
||||||
val callback = object : WaitListNode.Callback {
|
|
||||||
override fun onCancelClick() {
|
|
||||||
navigateUp()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
createNode<WaitListNode>(buildContext, plugins = listOf(callback, inputs))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023, 2024 New Vector Ltd.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
* Please see LICENSE in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package io.element.android.features.login.impl.error
|
|
||||||
|
|
||||||
import io.element.android.libraries.core.bool.orFalse
|
|
||||||
|
|
||||||
fun Throwable.isWaitListError(): Boolean {
|
|
||||||
return message?.contains("IO_ELEMENT_X_WAIT_LIST").orFalse()
|
|
||||||
}
|
|
||||||
|
|
@ -12,7 +12,6 @@ import androidx.compose.ui.Modifier
|
||||||
import com.bumble.appyx.core.modality.BuildContext
|
import com.bumble.appyx.core.modality.BuildContext
|
||||||
import com.bumble.appyx.core.node.Node
|
import com.bumble.appyx.core.node.Node
|
||||||
import com.bumble.appyx.core.plugin.Plugin
|
import com.bumble.appyx.core.plugin.Plugin
|
||||||
import com.bumble.appyx.core.plugin.plugins
|
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import io.element.android.anvilannotations.ContributesNode
|
import io.element.android.anvilannotations.ContributesNode
|
||||||
|
|
@ -24,14 +23,6 @@ class LoginPasswordNode @AssistedInject constructor(
|
||||||
@Assisted plugins: List<Plugin>,
|
@Assisted plugins: List<Plugin>,
|
||||||
private val presenter: LoginPasswordPresenter,
|
private val presenter: LoginPasswordPresenter,
|
||||||
) : Node(buildContext, plugins = plugins) {
|
) : Node(buildContext, plugins = plugins) {
|
||||||
interface Callback : Plugin {
|
|
||||||
fun onWaitListError(loginFormState: LoginFormState)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onWaitListError(loginFormState: LoginFormState) {
|
|
||||||
plugins<Callback>().forEach { it.onWaitListError(loginFormState) }
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun View(modifier: Modifier) {
|
override fun View(modifier: Modifier) {
|
||||||
val state = presenter.present()
|
val state = presenter.present()
|
||||||
|
|
@ -39,7 +30,6 @@ class LoginPasswordNode @AssistedInject constructor(
|
||||||
state = state,
|
state = state,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
onBackClick = ::navigateUp,
|
onBackClick = ::navigateUp,
|
||||||
onWaitListError = ::onWaitListError,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,6 @@ import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.compound.theme.ElementTheme
|
import io.element.android.compound.theme.ElementTheme
|
||||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||||
import io.element.android.features.login.impl.R
|
import io.element.android.features.login.impl.R
|
||||||
import io.element.android.features.login.impl.error.isWaitListError
|
|
||||||
import io.element.android.features.login.impl.error.loginError
|
import io.element.android.features.login.impl.error.loginError
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule
|
import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule
|
||||||
|
|
@ -72,7 +71,6 @@ import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
fun LoginPasswordView(
|
fun LoginPasswordView(
|
||||||
state: LoginPasswordState,
|
state: LoginPasswordState,
|
||||||
onBackClick: () -> Unit,
|
onBackClick: () -> Unit,
|
||||||
onWaitListError: (LoginFormState) -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val isLoading by remember(state.loginAction) {
|
val isLoading by remember(state.loginAction) {
|
||||||
|
|
@ -149,16 +147,9 @@ fun LoginPasswordView(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.loginAction is AsyncData.Failure) {
|
if (state.loginAction is AsyncData.Failure) {
|
||||||
when {
|
LoginErrorDialog(error = state.loginAction.error, onDismiss = {
|
||||||
state.loginAction.error.isWaitListError() -> {
|
state.eventSink(LoginPasswordEvents.ClearError)
|
||||||
onWaitListError(state.formState)
|
})
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
LoginErrorDialog(error = state.loginAction.error, onDismiss = {
|
|
||||||
state.eventSink(LoginPasswordEvents.ClearError)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -302,6 +293,5 @@ internal fun LoginPasswordViewPreview(@PreviewParameter(LoginPasswordStateProvid
|
||||||
LoginPasswordView(
|
LoginPasswordView(
|
||||||
state = state,
|
state = state,
|
||||||
onBackClick = {},
|
onBackClick = {},
|
||||||
onWaitListError = {},
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023, 2024 New Vector Ltd.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
* Please see LICENSE in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package io.element.android.features.login.impl.screens.waitlistscreen
|
|
||||||
|
|
||||||
sealed interface WaitListEvents {
|
|
||||||
data object AttemptLogin : WaitListEvents
|
|
||||||
data object ClearError : WaitListEvents
|
|
||||||
data object Continue : WaitListEvents
|
|
||||||
}
|
|
||||||
|
|
@ -1,52 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023, 2024 New Vector Ltd.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
* Please see LICENSE in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package io.element.android.features.login.impl.screens.waitlistscreen
|
|
||||||
|
|
||||||
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.features.login.impl.screens.loginpassword.LoginFormState
|
|
||||||
import io.element.android.libraries.architecture.NodeInputs
|
|
||||||
import io.element.android.libraries.architecture.inputs
|
|
||||||
import io.element.android.libraries.di.AppScope
|
|
||||||
|
|
||||||
@ContributesNode(AppScope::class)
|
|
||||||
class WaitListNode @AssistedInject constructor(
|
|
||||||
@Assisted buildContext: BuildContext,
|
|
||||||
@Assisted plugins: List<Plugin>,
|
|
||||||
presenterFactory: WaitListPresenter.Factory,
|
|
||||||
) : Node(buildContext, plugins = plugins) {
|
|
||||||
data class Inputs(val loginFormState: LoginFormState) : NodeInputs
|
|
||||||
|
|
||||||
private val inputs: Inputs = inputs()
|
|
||||||
private val presenter = presenterFactory.create(inputs.loginFormState)
|
|
||||||
|
|
||||||
interface Callback : Plugin {
|
|
||||||
fun onCancelClick()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onCancelClick() {
|
|
||||||
plugins<Callback>().forEach { it.onCancelClick() }
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
override fun View(modifier: Modifier) {
|
|
||||||
val state = presenter.present()
|
|
||||||
WaitListView(
|
|
||||||
state = state,
|
|
||||||
onCancelClick = ::onCancelClick,
|
|
||||||
modifier = modifier
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,87 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023, 2024 New Vector Ltd.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
* Please see LICENSE in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package io.element.android.features.login.impl.screens.waitlistscreen
|
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.MutableState
|
|
||||||
import androidx.compose.runtime.mutableIntStateOf
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
|
||||||
import dagger.assisted.Assisted
|
|
||||||
import dagger.assisted.AssistedFactory
|
|
||||||
import dagger.assisted.AssistedInject
|
|
||||||
import io.element.android.features.login.impl.DefaultLoginUserStory
|
|
||||||
import io.element.android.features.login.impl.screens.loginpassword.LoginFormState
|
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
|
||||||
import io.element.android.libraries.architecture.Presenter
|
|
||||||
import io.element.android.libraries.core.meta.BuildMeta
|
|
||||||
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
|
|
||||||
import io.element.android.libraries.matrix.api.core.SessionId
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import timber.log.Timber
|
|
||||||
|
|
||||||
class WaitListPresenter @AssistedInject constructor(
|
|
||||||
@Assisted private val formState: LoginFormState,
|
|
||||||
private val buildMeta: BuildMeta,
|
|
||||||
private val authenticationService: MatrixAuthenticationService,
|
|
||||||
private val defaultLoginUserStory: DefaultLoginUserStory,
|
|
||||||
) : Presenter<WaitListState> {
|
|
||||||
@AssistedFactory
|
|
||||||
interface Factory {
|
|
||||||
fun create(loginFormState: LoginFormState): WaitListPresenter
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
override fun present(): WaitListState {
|
|
||||||
val coroutineScope = rememberCoroutineScope()
|
|
||||||
val homeserverUrl = remember {
|
|
||||||
authenticationService.getHomeserverDetails().value?.url ?: "server"
|
|
||||||
}
|
|
||||||
|
|
||||||
val loginAction: MutableState<AsyncData<SessionId>> = remember {
|
|
||||||
mutableStateOf(AsyncData.Uninitialized)
|
|
||||||
}
|
|
||||||
|
|
||||||
val attemptNumber = remember { mutableIntStateOf(0) }
|
|
||||||
|
|
||||||
fun handleEvents(event: WaitListEvents) {
|
|
||||||
when (event) {
|
|
||||||
WaitListEvents.AttemptLogin -> {
|
|
||||||
// Do not attempt to login on first resume of the View.
|
|
||||||
attemptNumber.intValue++
|
|
||||||
if (attemptNumber.intValue > 1) {
|
|
||||||
coroutineScope.loginAttempt(formState, loginAction)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
WaitListEvents.ClearError -> loginAction.value = AsyncData.Uninitialized
|
|
||||||
WaitListEvents.Continue -> defaultLoginUserStory.setLoginFlowIsDone(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return WaitListState(
|
|
||||||
appName = buildMeta.applicationName,
|
|
||||||
serverName = homeserverUrl,
|
|
||||||
loginAction = loginAction.value,
|
|
||||||
eventSink = ::handleEvents
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun CoroutineScope.loginAttempt(formState: LoginFormState, loggedInState: MutableState<AsyncData<SessionId>>) = launch {
|
|
||||||
Timber.w("Attempt to login...")
|
|
||||||
loggedInState.value = AsyncData.Loading()
|
|
||||||
authenticationService.login(formState.login.trim(), formState.password)
|
|
||||||
.onSuccess { sessionId ->
|
|
||||||
loggedInState.value = AsyncData.Success(sessionId)
|
|
||||||
}
|
|
||||||
.onFailure { failure ->
|
|
||||||
loggedInState.value = AsyncData.Failure(failure)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023, 2024 New Vector Ltd.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
* Please see LICENSE in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package io.element.android.features.login.impl.screens.waitlistscreen
|
|
||||||
|
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
|
||||||
import io.element.android.libraries.matrix.api.core.SessionId
|
|
||||||
|
|
||||||
// Do not use default value, so no member get forgotten in the presenters.
|
|
||||||
data class WaitListState(
|
|
||||||
val appName: String,
|
|
||||||
val serverName: String,
|
|
||||||
val loginAction: AsyncData<SessionId>,
|
|
||||||
val eventSink: (WaitListEvents) -> Unit
|
|
||||||
)
|
|
||||||
|
|
@ -1,35 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023, 2024 New Vector Ltd.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
* Please see LICENSE in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package io.element.android.features.login.impl.screens.waitlistscreen
|
|
||||||
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
|
||||||
import io.element.android.libraries.matrix.api.core.SessionId
|
|
||||||
|
|
||||||
open class WaitListStateProvider : PreviewParameterProvider<WaitListState> {
|
|
||||||
override val values: Sequence<WaitListState>
|
|
||||||
get() = sequenceOf(
|
|
||||||
aWaitListState(loginAction = AsyncData.Uninitialized),
|
|
||||||
aWaitListState(loginAction = AsyncData.Loading()),
|
|
||||||
aWaitListState(loginAction = AsyncData.Failure(Throwable("error"))),
|
|
||||||
aWaitListState(loginAction = AsyncData.Failure(Throwable(message = "IO_ELEMENT_X_WAIT_LIST"))),
|
|
||||||
aWaitListState(loginAction = AsyncData.Success(SessionId("@alice:element.io"))),
|
|
||||||
// Add other state here
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun aWaitListState(
|
|
||||||
appName: String = "Element X",
|
|
||||||
serverName: String = "server.org",
|
|
||||||
loginAction: AsyncData<SessionId> = AsyncData.Uninitialized,
|
|
||||||
) = WaitListState(
|
|
||||||
appName = appName,
|
|
||||||
serverName = serverName,
|
|
||||||
loginAction = loginAction,
|
|
||||||
eventSink = {}
|
|
||||||
)
|
|
||||||
|
|
@ -1,143 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023, 2024 New Vector Ltd.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
* Please see LICENSE in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package io.element.android.features.login.impl.screens.waitlistscreen
|
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.material3.LocalContentColor
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.lifecycle.Lifecycle
|
|
||||||
import io.element.android.compound.theme.ElementTheme
|
|
||||||
import io.element.android.features.login.impl.R
|
|
||||||
import io.element.android.features.login.impl.error.isWaitListError
|
|
||||||
import io.element.android.features.login.impl.error.loginError
|
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
|
||||||
import io.element.android.libraries.designsystem.atomic.pages.SunsetPage
|
|
||||||
import io.element.android.libraries.designsystem.components.dialogs.RetryDialog
|
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
|
||||||
import io.element.android.libraries.designsystem.theme.components.Button
|
|
||||||
import io.element.android.libraries.designsystem.theme.components.TextButton
|
|
||||||
import io.element.android.libraries.designsystem.utils.OnLifecycleEvent
|
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
|
||||||
|
|
||||||
// Ref: https://www.figma.com/file/0MMNu7cTOzLOlWb7ctTkv3/Element-X?type=design&node-id=6761-148425
|
|
||||||
// Only the first screen can be displayed, since once logged in, this Node will be remove by the RootNode.
|
|
||||||
@Composable
|
|
||||||
fun WaitListView(
|
|
||||||
state: WaitListState,
|
|
||||||
onCancelClick: () -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
|
||||||
OnLifecycleEvent { _, event ->
|
|
||||||
when (event) {
|
|
||||||
Lifecycle.Event.ON_RESUME -> state.eventSink.invoke(WaitListEvents.AttemptLogin)
|
|
||||||
else -> Unit
|
|
||||||
}
|
|
||||||
}
|
|
||||||
WaitListContent(state, onCancelClick, modifier)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun WaitListError(state: WaitListState) {
|
|
||||||
// Display a dialog for error other than the waitlist error
|
|
||||||
state.loginAction.errorOrNull()?.let { error ->
|
|
||||||
if (error.isWaitListError().not()) {
|
|
||||||
RetryDialog(
|
|
||||||
content = stringResource(id = loginError(error)),
|
|
||||||
onRetry = {
|
|
||||||
state.eventSink.invoke(WaitListEvents.AttemptLogin)
|
|
||||||
},
|
|
||||||
onDismiss = {
|
|
||||||
state.eventSink.invoke(WaitListEvents.ClearError)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun WaitListContent(
|
|
||||||
state: WaitListState,
|
|
||||||
onCancelClick: () -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
|
||||||
Box(
|
|
||||||
modifier = modifier.fillMaxSize(),
|
|
||||||
) {
|
|
||||||
val title = stringResource(
|
|
||||||
when (state.loginAction) {
|
|
||||||
is AsyncData.Success -> R.string.screen_waitlist_title_success
|
|
||||||
else -> R.string.screen_waitlist_title
|
|
||||||
}
|
|
||||||
)
|
|
||||||
val subtitle = when (state.loginAction) {
|
|
||||||
is AsyncData.Success -> stringResource(
|
|
||||||
id = R.string.screen_waitlist_message_success,
|
|
||||||
state.appName,
|
|
||||||
)
|
|
||||||
else -> stringResource(
|
|
||||||
id = R.string.screen_waitlist_message,
|
|
||||||
state.appName,
|
|
||||||
state.serverName,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
SunsetPage(
|
|
||||||
isLoading = state.loginAction.isLoading(),
|
|
||||||
title = title,
|
|
||||||
subtitle = subtitle,
|
|
||||||
) {
|
|
||||||
OverallContent(state, onCancelClick)
|
|
||||||
}
|
|
||||||
WaitListError(state)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun OverallContent(
|
|
||||||
state: WaitListState,
|
|
||||||
onCancelClick: () -> Unit,
|
|
||||||
) {
|
|
||||||
Box(modifier = Modifier.fillMaxSize()) {
|
|
||||||
if (state.loginAction !is AsyncData.Success) {
|
|
||||||
CompositionLocalProvider(LocalContentColor provides ElementTheme.colors.textOnSolidPrimary) {
|
|
||||||
TextButton(
|
|
||||||
text = stringResource(CommonStrings.action_cancel),
|
|
||||||
onClick = onCancelClick,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (state.loginAction is AsyncData.Success) {
|
|
||||||
Button(
|
|
||||||
text = stringResource(id = CommonStrings.action_continue),
|
|
||||||
onClick = { state.eventSink.invoke(WaitListEvents.Continue) },
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.align(Alignment.BottomCenter)
|
|
||||||
.padding(bottom = 8.dp),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@PreviewsDayNight
|
|
||||||
@Composable
|
|
||||||
internal fun WaitListViewPreview(@PreviewParameter(WaitListStateProvider::class) state: WaitListState) = ElementPreview {
|
|
||||||
WaitListView(
|
|
||||||
state = state,
|
|
||||||
onCancelClick = {},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,114 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023, 2024 New Vector Ltd.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
* Please see LICENSE in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package io.element.android.features.login.impl.screens.waitlistscreen
|
|
||||||
|
|
||||||
import app.cash.molecule.RecompositionMode
|
|
||||||
import app.cash.molecule.moleculeFlow
|
|
||||||
import app.cash.turbine.test
|
|
||||||
import com.google.common.truth.Truth.assertThat
|
|
||||||
import io.element.android.features.login.impl.DefaultLoginUserStory
|
|
||||||
import io.element.android.features.login.impl.screens.loginpassword.LoginFormState
|
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
|
||||||
import io.element.android.libraries.matrix.api.core.SessionId
|
|
||||||
import io.element.android.libraries.matrix.test.A_HOMESERVER
|
|
||||||
import io.element.android.libraries.matrix.test.A_HOMESERVER_URL
|
|
||||||
import io.element.android.libraries.matrix.test.A_THROWABLE
|
|
||||||
import io.element.android.libraries.matrix.test.A_USER_ID
|
|
||||||
import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService
|
|
||||||
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
|
||||||
import io.element.android.tests.testutils.WarmUpRule
|
|
||||||
import kotlinx.coroutines.test.runTest
|
|
||||||
import org.junit.Rule
|
|
||||||
import org.junit.Test
|
|
||||||
|
|
||||||
class WaitListPresenterTest {
|
|
||||||
@get:Rule
|
|
||||||
val warmUpRule = WarmUpRule()
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `present - initial state`() = runTest {
|
|
||||||
val authenticationService = FakeMatrixAuthenticationService().apply {
|
|
||||||
givenHomeserver(A_HOMESERVER)
|
|
||||||
}
|
|
||||||
val loginUserStory = DefaultLoginUserStory()
|
|
||||||
val presenter = WaitListPresenter(
|
|
||||||
LoginFormState.Default,
|
|
||||||
aBuildMeta(applicationName = "Application Name"),
|
|
||||||
authenticationService,
|
|
||||||
loginUserStory,
|
|
||||||
)
|
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
val initialState = awaitItem()
|
|
||||||
assertThat(initialState.appName).isEqualTo("Application Name")
|
|
||||||
assertThat(initialState.serverName).isEqualTo(A_HOMESERVER_URL)
|
|
||||||
assertThat(initialState.loginAction).isEqualTo(AsyncData.Uninitialized)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `present - attempt login with error`() = runTest {
|
|
||||||
val authenticationService = FakeMatrixAuthenticationService().apply {
|
|
||||||
givenLoginError(A_THROWABLE)
|
|
||||||
}
|
|
||||||
val loginUserStory = DefaultLoginUserStory()
|
|
||||||
val presenter = WaitListPresenter(
|
|
||||||
LoginFormState.Default,
|
|
||||||
aBuildMeta(),
|
|
||||||
authenticationService,
|
|
||||||
loginUserStory,
|
|
||||||
)
|
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
val initialState = awaitItem()
|
|
||||||
// First usage of AttemptLogin, nothing should happen
|
|
||||||
initialState.eventSink.invoke(WaitListEvents.AttemptLogin)
|
|
||||||
expectNoEvents()
|
|
||||||
initialState.eventSink.invoke(WaitListEvents.AttemptLogin)
|
|
||||||
val submitState = awaitItem()
|
|
||||||
assertThat(submitState.loginAction).isInstanceOf(AsyncData.Loading::class.java)
|
|
||||||
val errorState = awaitItem()
|
|
||||||
assertThat(errorState.loginAction).isEqualTo(AsyncData.Failure<SessionId>(A_THROWABLE))
|
|
||||||
// Assert the error can be cleared
|
|
||||||
errorState.eventSink(WaitListEvents.ClearError)
|
|
||||||
val clearedState = awaitItem()
|
|
||||||
assertThat(clearedState.loginAction).isEqualTo(AsyncData.Uninitialized)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `present - attempt login with success`() = runTest {
|
|
||||||
val authenticationService = FakeMatrixAuthenticationService()
|
|
||||||
val loginUserStory = DefaultLoginUserStory().apply { setLoginFlowIsDone(false) }
|
|
||||||
val presenter = WaitListPresenter(
|
|
||||||
LoginFormState.Default,
|
|
||||||
aBuildMeta(),
|
|
||||||
authenticationService,
|
|
||||||
loginUserStory,
|
|
||||||
)
|
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
assertThat(loginUserStory.loginFlowIsDone.value).isFalse()
|
|
||||||
val initialState = awaitItem()
|
|
||||||
// First usage of AttemptLogin, nothing should happen
|
|
||||||
initialState.eventSink.invoke(WaitListEvents.AttemptLogin)
|
|
||||||
expectNoEvents()
|
|
||||||
initialState.eventSink.invoke(WaitListEvents.AttemptLogin)
|
|
||||||
val submitState = awaitItem()
|
|
||||||
assertThat(submitState.loginAction).isInstanceOf(AsyncData.Loading::class.java)
|
|
||||||
val successState = awaitItem()
|
|
||||||
assertThat(successState.loginAction).isEqualTo(AsyncData.Success(A_USER_ID))
|
|
||||||
assertThat(loginUserStory.loginFlowIsDone.value).isFalse()
|
|
||||||
successState.eventSink.invoke(WaitListEvents.Continue)
|
|
||||||
assertThat(loginUserStory.loginFlowIsDone.value).isTrue()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -38,7 +38,6 @@ class LoginScreen(private val authenticationService: MatrixAuthenticationService
|
||||||
state = state,
|
state = state,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
onBackClick = {},
|
onBackClick = {},
|
||||||
onWaitListError = {},
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:b2c858bc7e2beecc0d4c92df0b4ac61e1e3a975a072e0e75cfa1da6aaa32142c
|
|
||||||
size 140926
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:8524bdd37ac6eb784d82041b572d9cf3bb69cf3de318ba1d8abc45e9a239dad1
|
|
||||||
size 141726
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:a4fd2455de9a763e582e4977e36686c714dfb380737fc6193b4eab9ef8b64de9
|
|
||||||
size 58641
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:b2c858bc7e2beecc0d4c92df0b4ac61e1e3a975a072e0e75cfa1da6aaa32142c
|
|
||||||
size 140926
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:f7880a67dd818c44b519be634a4a33a4a0efc842f9ad5e4879f16acb7c8b1f5f
|
|
||||||
size 122622
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:fa9162336cb7d46a2517f4b4149a9f8389e69e410e3896cb6b973fc16a940adf
|
|
||||||
size 138058
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:ef852707489db29300b4bf8259a5d71d3c82ce26bcea3ddb00d037b1ba379423
|
|
||||||
size 138901
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:5946208785bcc7eff5e76048cd467e9bde6f4ff5874ef2dd2562d5bbb0844f93
|
|
||||||
size 59177
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:fa9162336cb7d46a2517f4b4149a9f8389e69e410e3896cb6b973fc16a940adf
|
|
||||||
size 138058
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:3b4fe8418f426d9953189908053407550ad6d83d45a548253d75507321f686c9
|
|
||||||
size 120721
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:3d81ad9f5a5983653d9266a2500b2c71dc7ca231d52a62d1c1f1b091849ede32
|
|
||||||
size 165501
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:1cb4f2cbdc2b7fca643a9debff50206d4aa9321894a86149d6c815fd4db7b593
|
|
||||||
size 166084
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:3b942ce79745a26aa2d4227c35aaad442b35c2f768915e3d9fabf45a04cd1d82
|
|
||||||
size 61267
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:3d81ad9f5a5983653d9266a2500b2c71dc7ca231d52a62d1c1f1b091849ede32
|
|
||||||
size 165501
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:aa47e394a5af6cad06496a5d36fa81b2e7d76dd9eaec45c3914f1ffce45f9f91
|
|
||||||
size 146617
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue