Merge pull request #781 from vector-im/feature/bma/waitingListScreen
Implement waiting list screen (error IO_ELEMENT_X_WAIT_LIST)
This commit is contained in:
commit
77e2ff4953
39 changed files with 873 additions and 43 deletions
|
|
@ -42,6 +42,7 @@ import io.element.android.appnav.intent.IntentResolver
|
|||
import io.element.android.appnav.intent.ResolvedIntent
|
||||
import io.element.android.appnav.root.RootPresenter
|
||||
import io.element.android.appnav.root.RootView
|
||||
import io.element.android.features.login.api.LoginUserStory
|
||||
import io.element.android.features.login.api.oidc.OidcAction
|
||||
import io.element.android.features.login.api.oidc.OidcActionFlow
|
||||
import io.element.android.features.preferences.api.CacheService
|
||||
|
|
@ -55,6 +56,7 @@ import io.element.android.libraries.di.AppScope
|
|||
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.first
|
||||
|
|
@ -74,6 +76,7 @@ class RootFlowNode @AssistedInject constructor(
|
|||
private val bugReportEntryPoint: BugReportEntryPoint,
|
||||
private val intentResolver: IntentResolver,
|
||||
private val oidcActionFlow: OidcActionFlow,
|
||||
private val loginUserStory: LoginUserStory,
|
||||
) :
|
||||
BackstackNode<RootFlowNode.NavTarget>(
|
||||
backstack = BackStack(
|
||||
|
|
@ -90,8 +93,7 @@ class RootFlowNode @AssistedInject constructor(
|
|||
}
|
||||
|
||||
private fun observeLoggedInState() {
|
||||
authenticationService.isLoggedIn()
|
||||
.distinctUntilChanged()
|
||||
isUserLoggedInFlow()
|
||||
.combine(
|
||||
cacheService.cacheIndex().onEach {
|
||||
Timber.v("cacheIndex=$it")
|
||||
|
|
@ -114,6 +116,16 @@ class RootFlowNode @AssistedInject constructor(
|
|||
.launchIn(lifecycleScope)
|
||||
}
|
||||
|
||||
private fun isUserLoggedInFlow(): Flow<Boolean> {
|
||||
return combine(
|
||||
authenticationService.isLoggedIn(),
|
||||
loginUserStory.loginFlowIsDone
|
||||
) { isLoggedIn, loginFlowIsDone ->
|
||||
isLoggedIn && loginFlowIsDone
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
}
|
||||
|
||||
private fun switchToLoggedInFlow(sessionId: SessionId, cacheIndex: Int) {
|
||||
backstack.safeRoot(NavTarget.LoggedInFlow(sessionId, cacheIndex))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.login.api
|
||||
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
interface LoginUserStory {
|
||||
val loginFlowIsDone: StateFlow<Boolean>
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* 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
|
||||
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.features.login.api.LoginUserStory
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.di.SingleIn
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import javax.inject.Inject
|
||||
|
||||
@SingleIn(AppScope::class)
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultLoginUserStory @Inject constructor() : LoginUserStory {
|
||||
// True by default, will be set to false when the login user story is started, and set to true again once it's done.
|
||||
override val loginFlowIsDone: MutableStateFlow<Boolean> = MutableStateFlow(true)
|
||||
|
||||
fun setLoginFlowIsDone(value: Boolean) {
|
||||
loginFlowIsDone.value = value
|
||||
}
|
||||
}
|
||||
|
|
@ -18,7 +18,6 @@ package io.element.android.features.login.impl
|
|||
|
||||
import android.app.Activity
|
||||
import android.os.Parcelable
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.ui.Modifier
|
||||
|
|
@ -28,6 +27,7 @@ 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.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.singleTop
|
||||
import dagger.assisted.Assisted
|
||||
|
|
@ -39,8 +39,10 @@ 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.LoginFormState
|
||||
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.waitlistscreen.WaitListNode
|
||||
import io.element.android.libraries.architecture.BackstackNode
|
||||
import io.element.android.libraries.architecture.NodeInputs
|
||||
import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler
|
||||
|
|
@ -58,6 +60,7 @@ class LoginFlowNode @AssistedInject constructor(
|
|||
private val customTabAvailabilityChecker: CustomTabAvailabilityChecker,
|
||||
private val customTabHandler: CustomTabHandler,
|
||||
private val accountProviderDataSource: AccountProviderDataSource,
|
||||
private val defaultLoginUserStory: DefaultLoginUserStory,
|
||||
) : BackstackNode<LoginFlowNode.NavTarget>(
|
||||
backstack = BackStack(
|
||||
initialElement = NavTarget.ConfirmAccountProvider,
|
||||
|
|
@ -75,6 +78,11 @@ class LoginFlowNode @AssistedInject constructor(
|
|||
|
||||
private val inputs: Inputs = inputs()
|
||||
|
||||
override fun onBuilt() {
|
||||
super.onBuilt()
|
||||
defaultLoginUserStory.setLoginFlowIsDone(false)
|
||||
}
|
||||
|
||||
sealed interface NavTarget : Parcelable {
|
||||
@Parcelize
|
||||
object ConfirmAccountProvider : NavTarget
|
||||
|
|
@ -88,6 +96,9 @@ class LoginFlowNode @AssistedInject constructor(
|
|||
@Parcelize
|
||||
object LoginPassword : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data class WaitList(val loginFormState: LoginFormState) : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data class OidcView(val oidcDetails: OidcDetails) : NavTarget
|
||||
}
|
||||
|
|
@ -144,12 +155,28 @@ class LoginFlowNode @AssistedInject constructor(
|
|||
createNode<SearchAccountProviderNode>(buildContext, plugins = listOf(callback))
|
||||
}
|
||||
NavTarget.LoginPassword -> {
|
||||
createNode<LoginPasswordNode>(buildContext, plugins = listOf())
|
||||
val callback = object : LoginPasswordNode.Callback {
|
||||
override fun onWaitListError(loginFormState: LoginFormState) {
|
||||
backstack.newRoot(NavTarget.WaitList(loginFormState))
|
||||
}
|
||||
}
|
||||
createNode<LoginPasswordNode>(buildContext, plugins = listOf(callback))
|
||||
}
|
||||
is NavTarget.OidcView -> {
|
||||
val input = OidcNode.Inputs(navTarget.oidcDetails)
|
||||
createNode<OidcNode>(buildContext, plugins = listOf(input))
|
||||
}
|
||||
is NavTarget.WaitList -> {
|
||||
val inputs = WaitListNode.Inputs(
|
||||
loginFormState = navTarget.loginFormState,
|
||||
)
|
||||
val callback = object : WaitListNode.Callback {
|
||||
override fun onCancelClicked() {
|
||||
navigateUp()
|
||||
}
|
||||
}
|
||||
createNode<WaitListNode>(buildContext, plugins = listOf(callback, inputs))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.login.impl.error
|
||||
|
||||
import io.element.android.libraries.core.bool.orFalse
|
||||
|
||||
fun Throwable.isWaitListError(): Boolean {
|
||||
return message?.contains("IO_ELEMENT_X_WAIT_LIST").orFalse()
|
||||
}
|
||||
|
|
@ -21,6 +21,7 @@ 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
|
||||
|
|
@ -33,13 +34,22 @@ class LoginPasswordNode @AssistedInject constructor(
|
|||
private val presenter: LoginPasswordPresenter,
|
||||
) : Node(buildContext, plugins = plugins) {
|
||||
|
||||
interface Callback : Plugin {
|
||||
fun onWaitListError(loginFormState: LoginFormState)
|
||||
}
|
||||
|
||||
private fun onWaitListError(loginFormState: LoginFormState) {
|
||||
plugins<Callback>().forEach { it.onWaitListError(loginFormState) }
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
val state = presenter.present()
|
||||
LoginPasswordView(
|
||||
state = state,
|
||||
modifier = modifier,
|
||||
onBackPressed = ::navigateUp
|
||||
onBackPressed = ::navigateUp,
|
||||
onWaitListError = ::onWaitListError,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +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.DefaultLoginUserStory
|
||||
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
|
|
@ -36,6 +37,7 @@ import javax.inject.Inject
|
|||
class LoginPasswordPresenter @Inject constructor(
|
||||
private val authenticationService: MatrixAuthenticationService,
|
||||
private val accountProviderDataSource: AccountProviderDataSource,
|
||||
private val defaultLoginUserStory: DefaultLoginUserStory,
|
||||
) : Presenter<LoginPasswordState> {
|
||||
|
||||
@Composable
|
||||
|
|
@ -77,6 +79,8 @@ class LoginPasswordPresenter @Inject constructor(
|
|||
loggedInState.value = Async.Loading()
|
||||
authenticationService.login(formState.login.trim(), formState.password)
|
||||
.onSuccess { sessionId ->
|
||||
// We will not navigate to the WaitList screen, so the login user story is done
|
||||
defaultLoginUserStory.setLoginFlowIsDone(true)
|
||||
loggedInState.value = Async.Success(sessionId)
|
||||
}
|
||||
.onFailure { failure ->
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ 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.isWaitListError
|
||||
import io.element.android.features.login.impl.error.loginError
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.designsystem.ElementTextStyles
|
||||
|
|
@ -82,8 +83,9 @@ import io.element.android.libraries.ui.strings.CommonStrings
|
|||
@Composable
|
||||
fun LoginPasswordView(
|
||||
state: LoginPasswordState,
|
||||
modifier: Modifier = Modifier,
|
||||
onBackPressed: () -> Unit,
|
||||
onWaitListError: (LoginFormState) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val isLoading by remember(state.loginAction) {
|
||||
derivedStateOf {
|
||||
|
|
@ -133,7 +135,8 @@ fun LoginPasswordView(
|
|||
subTitle = stringResource(id = R.string.screen_login_form_header)
|
||||
)
|
||||
Spacer(Modifier.height(32.dp))
|
||||
LoginForm(state = state,
|
||||
LoginForm(
|
||||
state = state,
|
||||
isLoading = isLoading,
|
||||
onSubmit = ::submit
|
||||
)
|
||||
|
|
@ -152,9 +155,16 @@ fun LoginPasswordView(
|
|||
}
|
||||
|
||||
if (state.loginAction is Async.Failure) {
|
||||
LoginErrorDialog(error = state.loginAction.error, onDismiss = {
|
||||
state.eventSink(LoginPasswordEvents.ClearError)
|
||||
})
|
||||
when {
|
||||
state.loginAction.error.isWaitListError() -> {
|
||||
onWaitListError(state.formState)
|
||||
}
|
||||
else -> {
|
||||
LoginErrorDialog(error = state.loginAction.error, onDismiss = {
|
||||
state.eventSink(LoginPasswordEvents.ClearError)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -269,6 +279,7 @@ internal fun LoginForm(
|
|||
@Composable
|
||||
internal fun LoginErrorDialog(error: Throwable, onDismiss: () -> Unit) {
|
||||
ErrorDialog(
|
||||
title = stringResource(id = CommonStrings.dialog_title_error),
|
||||
content = stringResource(loginError(error)),
|
||||
onDismiss = onDismiss
|
||||
)
|
||||
|
|
@ -288,6 +299,7 @@ internal fun LoginPasswordViewDarkPreview(@PreviewParameter(LoginPasswordStatePr
|
|||
private fun ContentToPreview(state: LoginPasswordState) {
|
||||
LoginPasswordView(
|
||||
state = state,
|
||||
onBackPressed = {}
|
||||
onBackPressed = {},
|
||||
onWaitListError = {},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.login.impl.screens.waitlistscreen
|
||||
|
||||
sealed interface WaitListEvents {
|
||||
object AttemptLogin : WaitListEvents
|
||||
object ClearError : WaitListEvents
|
||||
object Continue : WaitListEvents
|
||||
}
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
* 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.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 onCancelClicked()
|
||||
}
|
||||
|
||||
private fun onCancelClicked() {
|
||||
plugins<Callback>().forEach { it.onCancelClicked() }
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
val state = presenter.present()
|
||||
WaitListView(
|
||||
state = state,
|
||||
onCancelClicked = ::onCancelClicked,
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
/*
|
||||
* 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.waitlistscreen
|
||||
|
||||
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 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.Async
|
||||
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<Async<SessionId>> = remember {
|
||||
mutableStateOf(Async.Uninitialized)
|
||||
}
|
||||
|
||||
val attemptNumber: MutableState<Int> = remember { mutableStateOf(0) }
|
||||
|
||||
fun handleEvents(event: WaitListEvents) {
|
||||
when (event) {
|
||||
WaitListEvents.AttemptLogin -> {
|
||||
// Do not attempt to login on first resume of the View.
|
||||
attemptNumber.value++
|
||||
if (attemptNumber.value > 1) {
|
||||
coroutineScope.loginAttempt(formState, loginAction)
|
||||
}
|
||||
}
|
||||
WaitListEvents.ClearError -> loginAction.value = Async.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<Async<SessionId>>) = launch {
|
||||
Timber.w("Attempt to login...")
|
||||
loggedInState.value = Async.Loading()
|
||||
authenticationService.login(formState.login.trim(), formState.password)
|
||||
.onSuccess { sessionId ->
|
||||
loggedInState.value = Async.Success(sessionId)
|
||||
}
|
||||
.onFailure { failure ->
|
||||
loggedInState.value = Async.Failure(failure)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* 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.waitlistscreen
|
||||
|
||||
import io.element.android.libraries.architecture.Async
|
||||
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: Async<SessionId>,
|
||||
val eventSink: (WaitListEvents) -> Unit
|
||||
)
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* 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.waitlistscreen
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
|
||||
open class WaitListStateProvider : PreviewParameterProvider<WaitListState> {
|
||||
override val values: Sequence<WaitListState>
|
||||
get() = sequenceOf(
|
||||
aWaitListState(loginAction = Async.Uninitialized),
|
||||
aWaitListState(loginAction = Async.Loading()),
|
||||
aWaitListState(loginAction = Async.Failure(Throwable())),
|
||||
aWaitListState(loginAction = Async.Failure(Throwable(message = "IO_ELEMENT_X_WAIT_LIST"))),
|
||||
aWaitListState(loginAction = Async.Success(SessionId("@alice:element.io"))),
|
||||
// Add other state here
|
||||
)
|
||||
}
|
||||
|
||||
fun aWaitListState(
|
||||
appName: String = "Element X",
|
||||
serverName: String = "server.org",
|
||||
loginAction: Async<SessionId> = Async.Uninitialized,
|
||||
) = WaitListState(
|
||||
appName = appName,
|
||||
serverName = serverName,
|
||||
loginAction = loginAction,
|
||||
eventSink = {}
|
||||
)
|
||||
|
|
@ -0,0 +1,266 @@
|
|||
/*
|
||||
* 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.waitlistscreen
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.systemBarsPadding
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.BiasAbsoluteAlignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
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 androidx.lifecycle.Lifecycle
|
||||
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.Async
|
||||
import io.element.android.libraries.designsystem.components.dialogs.RetryDialog
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
import io.element.android.libraries.designsystem.theme.components.Button
|
||||
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.designsystem.theme.components.TextButton
|
||||
import io.element.android.libraries.designsystem.utils.OnLifecycleEvent
|
||||
import io.element.android.libraries.theme.ElementTheme
|
||||
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,
|
||||
onCancelClicked: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
OnLifecycleEvent { _, event ->
|
||||
when (event) {
|
||||
Lifecycle.Event.ON_RESUME -> state.eventSink.invoke(WaitListEvents.AttemptLogin)
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
Box(modifier = modifier) {
|
||||
WaitListBackground()
|
||||
WaitListContent(state, onCancelClicked)
|
||||
WaitListError(state)
|
||||
}
|
||||
}
|
||||
|
||||
@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 WaitListBackground(
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(modifier = modifier.fillMaxSize()) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(0.3f)
|
||||
.background(Color.White)
|
||||
)
|
||||
Image(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
painter = painterResource(id = R.drawable.light_dark),
|
||||
contentScale = ContentScale.Crop,
|
||||
contentDescription = null,
|
||||
)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(0.7f)
|
||||
.background(Color(0xFF121418))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun WaitListContent(
|
||||
state: WaitListState,
|
||||
onCancelClicked: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.systemBarsPadding()
|
||||
.padding(horizontal = 16.dp, vertical = 16.dp)
|
||||
) {
|
||||
if (state.loginAction !is Async.Success) {
|
||||
TextButton(
|
||||
onClick = onCancelClicked,
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = Color.White,
|
||||
contentColor = Color.Black,
|
||||
disabledContainerColor = Color.White,
|
||||
disabledContentColor = Color.Black,
|
||||
),
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(CommonStrings.action_cancel),
|
||||
style = ElementTheme.typography.fontBodyLgMedium,
|
||||
)
|
||||
}
|
||||
}
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = BiasAbsoluteAlignment(
|
||||
horizontalBias = 0f,
|
||||
verticalBias = -0.05f
|
||||
)
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
if (state.loginAction.isLoading()) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(24.dp),
|
||||
strokeWidth = 2.dp,
|
||||
color = Color.White
|
||||
)
|
||||
} else {
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
}
|
||||
Spacer(modifier = Modifier.height(18.dp))
|
||||
val titleRes = when (state.loginAction) {
|
||||
is Async.Success -> R.string.screen_waitlist_title_success
|
||||
else -> R.string.screen_waitlist_title
|
||||
}
|
||||
Text(
|
||||
text = withColoredPeriod(titleRes),
|
||||
style = ElementTheme.typography.fontHeadingXlBold,
|
||||
textAlign = TextAlign.Center,
|
||||
color = Color.White,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
val subtitle = when (state.loginAction) {
|
||||
is Async.Success -> stringResource(
|
||||
id = R.string.screen_waitlist_message_success,
|
||||
state.appName,
|
||||
)
|
||||
else -> stringResource(
|
||||
id = R.string.screen_waitlist_message,
|
||||
state.appName,
|
||||
state.serverName,
|
||||
)
|
||||
}
|
||||
Text(
|
||||
modifier = Modifier.widthIn(max = 360.dp),
|
||||
text = subtitle,
|
||||
style = ElementTheme.typography.fontBodyLgRegular,
|
||||
textAlign = TextAlign.Center,
|
||||
color = Color.White,
|
||||
)
|
||||
}
|
||||
}
|
||||
if (state.loginAction is Async.Success) {
|
||||
Button(
|
||||
onClick = { state.eventSink.invoke(WaitListEvents.Continue) },
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = Color.White,
|
||||
contentColor = Color.Black,
|
||||
disabledContainerColor = Color.White,
|
||||
disabledContentColor = Color.Black,
|
||||
),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.align(Alignment.BottomCenter)
|
||||
.padding(bottom = 8.dp)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = CommonStrings.action_continue),
|
||||
style = ElementTheme.typography.fontBodyLgMedium,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun withColoredPeriod(
|
||||
@StringRes textRes: Int,
|
||||
) = buildAnnotatedString {
|
||||
val text = stringResource(textRes)
|
||||
append(text)
|
||||
if (text.endsWith(".")) {
|
||||
addStyle(
|
||||
style = SpanStyle(
|
||||
// Light.colorGreen700
|
||||
color = Color(0xff0bc491),
|
||||
),
|
||||
start = text.length - 1,
|
||||
end = text.length,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun WaitListViewLightPreview(@PreviewParameter(WaitListStateProvider::class) state: WaitListState) =
|
||||
ElementPreviewLight { ContentToPreview(state) }
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun WaitListViewDarkPreview(@PreviewParameter(WaitListStateProvider::class) state: WaitListState) =
|
||||
ElementPreviewDark { ContentToPreview(state) }
|
||||
|
||||
@Composable
|
||||
private fun ContentToPreview(state: WaitListState) {
|
||||
WaitListView(
|
||||
state = state,
|
||||
onCancelClicked = {},
|
||||
)
|
||||
}
|
||||
BIN
features/login/impl/src/main/res/drawable/light_dark.png
Normal file
BIN
features/login/impl/src/main/res/drawable/light_dark.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 104 KiB |
|
|
@ -33,6 +33,12 @@
|
|||
<string name="screen_server_confirmation_message_register">"Zde budou uloženy vaše konverzace - podobně jako u poskytovatele e-mailových služeb uchováváte své e-maily."</string>
|
||||
<string name="screen_server_confirmation_title_login">"Chystáte se přihlásit do služby %1$s"</string>
|
||||
<string name="screen_server_confirmation_title_register">"Chystáte se vytvořit účet na %1$s"</string>
|
||||
<string name="screen_waitlist_message">"Na %2$s je momentálně vysoká poptávka po %1$s. Vraťte se do aplikace za pár dní a zkuste to znovu.
|
||||
|
||||
Díky za trpělivost!"</string>
|
||||
<string name="screen_waitlist_message_success">"Vítá vás %1$s"</string>
|
||||
<string name="screen_waitlist_title">"Jste v pořadníku!"</string>
|
||||
<string name="screen_waitlist_title_success">"Jdete do toho!"</string>
|
||||
<string name="screen_change_server_submit">"Pokračovat"</string>
|
||||
<string name="screen_change_server_title">"Vyberte svůj server"</string>
|
||||
<string name="screen_login_password_hint">"Heslo"</string>
|
||||
|
|
|
|||
|
|
@ -33,6 +33,12 @@
|
|||
<string name="screen_server_confirmation_message_register">"Hier werden deine Konversationen stattfinden — genau so wie du einen E-Mail-Anbieter verwenden würdest, um deine E-Mails aufzubewahren."</string>
|
||||
<string name="screen_server_confirmation_title_login">"Du bist dabei dich bei %1$s anzumelden"</string>
|
||||
<string name="screen_server_confirmation_title_register">"Du bist dabei einen Account auf %1$s zu erstellen"</string>
|
||||
<string name="screen_waitlist_message">"Im Moment besteht eine hohe Nachfrage nach %1$s auf %2$s. Besuche die App in ein paar Tagen wieder und versuche es erneut.
|
||||
|
||||
Vielen Dank für deine Geduld!"</string>
|
||||
<string name="screen_waitlist_message_success">"Willkommen bei %1$s!"</string>
|
||||
<string name="screen_waitlist_title">"Du hast es fast geschafft!"</string>
|
||||
<string name="screen_waitlist_title_success">"Du bist dabei."</string>
|
||||
<string name="screen_change_server_submit">"Weiter"</string>
|
||||
<string name="screen_change_server_title">"Wählen deinen Server"</string>
|
||||
<string name="screen_login_password_hint">"Passwort"</string>
|
||||
|
|
|
|||
|
|
@ -33,6 +33,12 @@
|
|||
<string name="screen_server_confirmation_message_register">"Aici vor trăi conversațiile dvs. - la fel cum ați folosi un furnizor de e-mail pentru a vă păstra e-mailurile."</string>
|
||||
<string name="screen_server_confirmation_title_login">"Sunteți pe cale să vă conectați la %1$s"</string>
|
||||
<string name="screen_server_confirmation_title_register">"Sunteți pe cale să creați un cont pe %1$s"</string>
|
||||
<string name="screen_waitlist_message">"Există o cerere mare pentru %1$s pentru %2$s în acest moment. Reveniți la aplicație în câteva zile și încercați din nou.
|
||||
|
||||
Vă mulțumim pentru răbdare!"</string>
|
||||
<string name="screen_waitlist_message_success">"Bun venit la %1$s"</string>
|
||||
<string name="screen_waitlist_title">"Sunteți pe lista de așteptare"</string>
|
||||
<string name="screen_waitlist_title_success">"Sunteți conectat!"</string>
|
||||
<string name="screen_change_server_submit">"Continuați"</string>
|
||||
<string name="screen_change_server_title">"Selectați serverul"</string>
|
||||
<string name="screen_login_password_hint">"Parola"</string>
|
||||
|
|
|
|||
|
|
@ -33,6 +33,12 @@
|
|||
<string name="screen_server_confirmation_message_register">"Tu budú žiť vaše konverzácie - podobne ako používate poskytovateľa e-mailových služieb na uchovávanie e-mailov."</string>
|
||||
<string name="screen_server_confirmation_title_login">"Chystáte sa prihlásiť do %1$s"</string>
|
||||
<string name="screen_server_confirmation_title_register">"Chystáte sa vytvoriť účet na %1$s"</string>
|
||||
<string name="screen_waitlist_message">"Momentálne je veľký dopyt po %1$s na %2$s. Vráťte sa do aplikácie za pár dní a skúste to znova.
|
||||
|
||||
Ďakujeme za trpezlivosť!"</string>
|
||||
<string name="screen_waitlist_message_success">"Vitajte v %1$s"</string>
|
||||
<string name="screen_waitlist_title">"Ste na čakanej listine!"</string>
|
||||
<string name="screen_waitlist_title_success">"Ste dnu!"</string>
|
||||
<string name="screen_change_server_submit">"Pokračovať"</string>
|
||||
<string name="screen_change_server_title">"Vyberte svoj server"</string>
|
||||
<string name="screen_login_password_hint">"Heslo"</string>
|
||||
|
|
|
|||
|
|
@ -33,6 +33,12 @@
|
|||
<string name="screen_server_confirmation_message_register">"This is where your conversations will live — just like you would use an email provider to keep your emails."</string>
|
||||
<string name="screen_server_confirmation_title_login">"You’re about to sign in to %1$s"</string>
|
||||
<string name="screen_server_confirmation_title_register">"You’re about to create an account on %1$s"</string>
|
||||
<string name="screen_waitlist_message">"There\'s a high demand for %1$s on %2$s at the moment. Come back to the app in a few days and try again.
|
||||
|
||||
Thanks for your patience!"</string>
|
||||
<string name="screen_waitlist_message_success">"Welcome to %1$s!"</string>
|
||||
<string name="screen_waitlist_title">"You’re almost there."</string>
|
||||
<string name="screen_waitlist_title_success">"You\'re in."</string>
|
||||
<string name="screen_change_server_submit">"Continue"</string>
|
||||
<string name="screen_change_server_title">"Select your server"</string>
|
||||
<string name="screen_login_password_hint">"Password"</string>
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ 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.DefaultLoginUserStory
|
||||
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
|
||||
|
|
@ -38,9 +39,11 @@ class LoginPasswordPresenterTest {
|
|||
fun `present - initial state`() = runTest {
|
||||
val authenticationService = FakeAuthenticationService()
|
||||
val accountProviderDataSource = AccountProviderDataSource()
|
||||
val loginUserStory = DefaultLoginUserStory()
|
||||
val presenter = LoginPasswordPresenter(
|
||||
authenticationService,
|
||||
accountProviderDataSource,
|
||||
loginUserStory,
|
||||
)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
|
|
@ -57,9 +60,11 @@ class LoginPasswordPresenterTest {
|
|||
fun `present - enter login and password`() = runTest {
|
||||
val authenticationService = FakeAuthenticationService()
|
||||
val accountProviderDataSource = AccountProviderDataSource()
|
||||
val loginUserStory = DefaultLoginUserStory()
|
||||
val presenter = LoginPasswordPresenter(
|
||||
authenticationService,
|
||||
accountProviderDataSource,
|
||||
loginUserStory,
|
||||
)
|
||||
authenticationService.givenHomeserver(A_HOMESERVER)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
|
|
@ -81,14 +86,17 @@ class LoginPasswordPresenterTest {
|
|||
fun `present - submit`() = runTest {
|
||||
val authenticationService = FakeAuthenticationService()
|
||||
val accountProviderDataSource = AccountProviderDataSource()
|
||||
val loginUserStory = DefaultLoginUserStory().apply { setLoginFlowIsDone(false) }
|
||||
val presenter = LoginPasswordPresenter(
|
||||
authenticationService,
|
||||
accountProviderDataSource,
|
||||
loginUserStory,
|
||||
)
|
||||
authenticationService.givenHomeserver(A_HOMESERVER)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
assertThat(loginUserStory.loginFlowIsDone.value).isFalse()
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink.invoke(LoginPasswordEvents.SetLogin(A_USER_NAME))
|
||||
initialState.eventSink.invoke(LoginPasswordEvents.SetPassword(A_PASSWORD))
|
||||
|
|
@ -99,6 +107,7 @@ class LoginPasswordPresenterTest {
|
|||
assertThat(submitState.loginAction).isInstanceOf(Async.Loading::class.java)
|
||||
val loggedInState = awaitItem()
|
||||
assertThat(loggedInState.loginAction).isEqualTo(Async.Success(A_SESSION_ID))
|
||||
assertThat(loginUserStory.loginFlowIsDone.value).isTrue()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -106,9 +115,11 @@ class LoginPasswordPresenterTest {
|
|||
fun `present - submit with error`() = runTest {
|
||||
val authenticationService = FakeAuthenticationService()
|
||||
val accountProviderDataSource = AccountProviderDataSource()
|
||||
val loginUserStory = DefaultLoginUserStory()
|
||||
val presenter = LoginPasswordPresenter(
|
||||
authenticationService,
|
||||
accountProviderDataSource,
|
||||
loginUserStory,
|
||||
)
|
||||
authenticationService.givenHomeserver(A_HOMESERVER)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
|
|
@ -132,9 +143,11 @@ class LoginPasswordPresenterTest {
|
|||
fun `present - clear error`() = runTest {
|
||||
val authenticationService = FakeAuthenticationService()
|
||||
val accountProviderDataSource = AccountProviderDataSource()
|
||||
val loginUserStory = DefaultLoginUserStory()
|
||||
val presenter = LoginPasswordPresenter(
|
||||
authenticationService,
|
||||
accountProviderDataSource,
|
||||
loginUserStory,
|
||||
)
|
||||
authenticationService.givenHomeserver(A_HOMESERVER)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,118 @@
|
|||
/*
|
||||
* 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.waitlistscreen
|
||||
|
||||
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.DefaultLoginUserStory
|
||||
import io.element.android.features.login.impl.screens.loginpassword.LoginFormState
|
||||
import io.element.android.libraries.architecture.Async
|
||||
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.FakeAuthenticationService
|
||||
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
class WaitListPresenterTest {
|
||||
@Test
|
||||
fun `present - initial state`() = runTest {
|
||||
val authenticationService = FakeAuthenticationService().apply {
|
||||
givenHomeserver(A_HOMESERVER)
|
||||
}
|
||||
val loginUserStory = DefaultLoginUserStory()
|
||||
val presenter = WaitListPresenter(
|
||||
LoginFormState.Default,
|
||||
aBuildMeta(applicationName = "Application Name"),
|
||||
authenticationService,
|
||||
loginUserStory,
|
||||
)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.appName).isEqualTo("Application Name")
|
||||
assertThat(initialState.serverName).isEqualTo(A_HOMESERVER_URL)
|
||||
assertThat(initialState.loginAction).isEqualTo(Async.Uninitialized)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - attempt login with error`() = runTest {
|
||||
val authenticationService = FakeAuthenticationService().apply {
|
||||
givenLoginError(A_THROWABLE)
|
||||
}
|
||||
val loginUserStory = DefaultLoginUserStory()
|
||||
val presenter = WaitListPresenter(
|
||||
LoginFormState.Default,
|
||||
aBuildMeta(),
|
||||
authenticationService,
|
||||
loginUserStory,
|
||||
)
|
||||
moleculeFlow(RecompositionClock.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(Async.Loading::class.java)
|
||||
val errorState = awaitItem()
|
||||
assertThat(errorState.loginAction).isEqualTo(Async.Failure<SessionId>(A_THROWABLE))
|
||||
// Assert the error can be cleared
|
||||
errorState.eventSink(WaitListEvents.ClearError)
|
||||
val clearedState = awaitItem()
|
||||
assertThat(clearedState.loginAction).isEqualTo(Async.Uninitialized)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - attempt login with success`() = runTest {
|
||||
val authenticationService = FakeAuthenticationService()
|
||||
val loginUserStory = DefaultLoginUserStory().apply { setLoginFlowIsDone(false) }
|
||||
val presenter = WaitListPresenter(
|
||||
LoginFormState.Default,
|
||||
aBuildMeta(),
|
||||
authenticationService,
|
||||
loginUserStory,
|
||||
)
|
||||
moleculeFlow(RecompositionClock.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(Async.Loading::class.java)
|
||||
val successState = awaitItem()
|
||||
assertThat(successState.loginAction).isEqualTo(Async.Success(A_USER_ID))
|
||||
assertThat(loginUserStory.loginFlowIsDone.value).isFalse()
|
||||
successState.eventSink.invoke(WaitListEvents.Continue)
|
||||
assertThat(loginUserStory.loginFlowIsDone.value).isTrue()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -175,12 +175,6 @@
|
|||
<string name="screen_share_open_osm_maps">"Otevřít v OpenStreetMap"</string>
|
||||
<string name="screen_share_this_location_action">"Sdílet tuto polohu"</string>
|
||||
<string name="screen_view_location_title">"Poloha"</string>
|
||||
<string name="screen_waitlist_message">"Na %2$s je momentálně vysoká poptávka po %1$s. Vraťte se do aplikace za pár dní a zkuste to znovu.
|
||||
|
||||
Díky za trpělivost!"</string>
|
||||
<string name="screen_waitlist_message_success">"Vítá vás %1$s"</string>
|
||||
<string name="screen_waitlist_title">"Jste v pořadníku!"</string>
|
||||
<string name="screen_waitlist_title_success">"Jdete do toho!"</string>
|
||||
<string name="settings_rageshake">"Rageshake"</string>
|
||||
<string name="settings_rageshake_detection_threshold">"Práh detekce"</string>
|
||||
<string name="settings_title_general">"Obecné"</string>
|
||||
|
|
|
|||
|
|
@ -174,12 +174,6 @@
|
|||
<string name="screen_share_open_osm_maps">"In OpenStreetMap öffnen"</string>
|
||||
<string name="screen_share_this_location_action">"Diesen Ort teilen"</string>
|
||||
<string name="screen_view_location_title">"Standort"</string>
|
||||
<string name="screen_waitlist_message">"Im Moment besteht eine hohe Nachfrage nach %1$s auf %2$s. Besuche die App in ein paar Tagen wieder und versuche es erneut.
|
||||
|
||||
Vielen Dank für deine Geduld!"</string>
|
||||
<string name="screen_waitlist_message_success">"Willkommen bei %1$s!"</string>
|
||||
<string name="screen_waitlist_title">"Du hast es fast geschafft!"</string>
|
||||
<string name="screen_waitlist_title_success">"Du bist dabei."</string>
|
||||
<string name="settings_rageshake">"Rageshake"</string>
|
||||
<string name="settings_rageshake_detection_threshold">"Erkennungsschwelle"</string>
|
||||
<string name="settings_title_general">"Allgemein"</string>
|
||||
|
|
|
|||
|
|
@ -170,12 +170,6 @@
|
|||
<string name="screen_share_my_location_action">"Distribuiți locația mea"</string>
|
||||
<string name="screen_share_this_location_action">"Distribuiți această locație"</string>
|
||||
<string name="screen_view_location_title">"Locație"</string>
|
||||
<string name="screen_waitlist_message">"Există o cerere mare pentru %1$s pentru %2$s în acest moment. Reveniți la aplicație în câteva zile și încercați din nou.
|
||||
|
||||
Vă mulțumim pentru răbdare!"</string>
|
||||
<string name="screen_waitlist_message_success">"Bun venit la %1$s"</string>
|
||||
<string name="screen_waitlist_title">"Sunteți pe lista de așteptare"</string>
|
||||
<string name="screen_waitlist_title_success">"Sunteți conectat!"</string>
|
||||
<string name="settings_rageshake">"Rageshake"</string>
|
||||
<string name="settings_rageshake_detection_threshold">"Prag de detecție"</string>
|
||||
<string name="settings_title_general">"General"</string>
|
||||
|
|
|
|||
|
|
@ -175,12 +175,6 @@
|
|||
<string name="screen_share_open_osm_maps">"Otvoriť v OpenStreetMap"</string>
|
||||
<string name="screen_share_this_location_action">"Zdieľajte túto polohu"</string>
|
||||
<string name="screen_view_location_title">"Poloha"</string>
|
||||
<string name="screen_waitlist_message">"Momentálne je veľký dopyt po %1$s na %2$s. Vráťte sa do aplikácie za pár dní a skúste to znova.
|
||||
|
||||
Ďakujeme za trpezlivosť!"</string>
|
||||
<string name="screen_waitlist_message_success">"Vitajte v %1$s"</string>
|
||||
<string name="screen_waitlist_title">"Ste na čakanej listine!"</string>
|
||||
<string name="screen_waitlist_title_success">"Ste dnu!"</string>
|
||||
<string name="settings_rageshake">"Zúrivé potrasenie"</string>
|
||||
<string name="settings_rageshake_detection_threshold">"Prahová hodnota detekcie"</string>
|
||||
<string name="settings_title_general">"Všeobecné"</string>
|
||||
|
|
|
|||
|
|
@ -174,12 +174,6 @@
|
|||
<string name="screen_share_open_osm_maps">"Open in OpenStreetMap"</string>
|
||||
<string name="screen_share_this_location_action">"Share this location"</string>
|
||||
<string name="screen_view_location_title">"Location"</string>
|
||||
<string name="screen_waitlist_message">"There\'s a high demand for %1$s on %2$s at the moment. Come back to the app in a few days and try again.
|
||||
|
||||
Thanks for your patience!"</string>
|
||||
<string name="screen_waitlist_message_success">"Welcome to %1$s!"</string>
|
||||
<string name="screen_waitlist_title">"You’re almost there."</string>
|
||||
<string name="screen_waitlist_title_success">"You\'re in."</string>
|
||||
<string name="settings_rageshake">"Rageshake"</string>
|
||||
<string name="settings_rageshake_detection_threshold">"Detection threshold"</string>
|
||||
<string name="settings_title_general">"General"</string>
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ 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.DefaultLoginUserStory
|
||||
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
|
||||
|
|
@ -33,7 +34,8 @@ class LoginScreen(private val authenticationService: MatrixAuthenticationService
|
|||
val presenter = remember {
|
||||
LoginPasswordPresenter(
|
||||
authenticationService = authenticationService,
|
||||
AccountProviderDataSource()
|
||||
AccountProviderDataSource(),
|
||||
DefaultLoginUserStory(),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -46,6 +48,7 @@ class LoginScreen(private val authenticationService: MatrixAuthenticationService
|
|||
state = state,
|
||||
modifier = modifier,
|
||||
onBackPressed = {},
|
||||
onWaitListError = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e97d88bef72c332cd145dc1080a989d163478c0252a9993f2f474bd51f2e4da8
|
||||
size 148762
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e8edd6d72db9efaaed76ac64f9882a5b66ac30747355815955157a3f3fc98c2c
|
||||
size 149344
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e099a30e3090ffd9c0f30e095131cb60d53b615623acd50565bde8fa28e7fe74
|
||||
size 62810
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e97d88bef72c332cd145dc1080a989d163478c0252a9993f2f474bd51f2e4da8
|
||||
size 148762
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:85222447b700315e6eea458fc72d61fb741018cb80c3a4530d4efc08ac9335ac
|
||||
size 129373
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e97d88bef72c332cd145dc1080a989d163478c0252a9993f2f474bd51f2e4da8
|
||||
size 148762
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e8edd6d72db9efaaed76ac64f9882a5b66ac30747355815955157a3f3fc98c2c
|
||||
size 149344
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:71289cbba79500c063ec80430fb46bc544ccbe6fa7035ac81c81322c9f1a300f
|
||||
size 63631
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e97d88bef72c332cd145dc1080a989d163478c0252a9993f2f474bd51f2e4da8
|
||||
size 148762
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:85222447b700315e6eea458fc72d61fb741018cb80c3a4530d4efc08ac9335ac
|
||||
size 129373
|
||||
|
|
@ -76,7 +76,8 @@
|
|||
"screen_server_confirmation_.*",
|
||||
"screen_change_server_.*",
|
||||
"screen_change_account_provider_.*",
|
||||
"screen_account_provider_.*"
|
||||
"screen_account_provider_.*",
|
||||
"screen_waitlist_.*"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue