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.intent.ResolvedIntent
|
||||||
import io.element.android.appnav.root.RootPresenter
|
import io.element.android.appnav.root.RootPresenter
|
||||||
import io.element.android.appnav.root.RootView
|
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.OidcAction
|
||||||
import io.element.android.features.login.api.oidc.OidcActionFlow
|
import io.element.android.features.login.api.oidc.OidcActionFlow
|
||||||
import io.element.android.features.preferences.api.CacheService
|
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.auth.MatrixAuthenticationService
|
||||||
import io.element.android.libraries.matrix.api.core.SessionId
|
import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
|
|
@ -74,6 +76,7 @@ class RootFlowNode @AssistedInject constructor(
|
||||||
private val bugReportEntryPoint: BugReportEntryPoint,
|
private val bugReportEntryPoint: BugReportEntryPoint,
|
||||||
private val intentResolver: IntentResolver,
|
private val intentResolver: IntentResolver,
|
||||||
private val oidcActionFlow: OidcActionFlow,
|
private val oidcActionFlow: OidcActionFlow,
|
||||||
|
private val loginUserStory: LoginUserStory,
|
||||||
) :
|
) :
|
||||||
BackstackNode<RootFlowNode.NavTarget>(
|
BackstackNode<RootFlowNode.NavTarget>(
|
||||||
backstack = BackStack(
|
backstack = BackStack(
|
||||||
|
|
@ -90,8 +93,7 @@ class RootFlowNode @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun observeLoggedInState() {
|
private fun observeLoggedInState() {
|
||||||
authenticationService.isLoggedIn()
|
isUserLoggedInFlow()
|
||||||
.distinctUntilChanged()
|
|
||||||
.combine(
|
.combine(
|
||||||
cacheService.cacheIndex().onEach {
|
cacheService.cacheIndex().onEach {
|
||||||
Timber.v("cacheIndex=$it")
|
Timber.v("cacheIndex=$it")
|
||||||
|
|
@ -114,6 +116,16 @@ class RootFlowNode @AssistedInject constructor(
|
||||||
.launchIn(lifecycleScope)
|
.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) {
|
private fun switchToLoggedInFlow(sessionId: SessionId, cacheIndex: Int) {
|
||||||
backstack.safeRoot(NavTarget.LoggedInFlow(sessionId, cacheIndex))
|
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.app.Activity
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.ui.Modifier
|
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.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
|
||||||
|
|
@ -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.oidc.webview.OidcNode
|
||||||
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.BackstackNode
|
import io.element.android.libraries.architecture.BackstackNode
|
||||||
import io.element.android.libraries.architecture.NodeInputs
|
import io.element.android.libraries.architecture.NodeInputs
|
||||||
import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler
|
import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler
|
||||||
|
|
@ -58,6 +60,7 @@ class LoginFlowNode @AssistedInject constructor(
|
||||||
private val customTabAvailabilityChecker: CustomTabAvailabilityChecker,
|
private val customTabAvailabilityChecker: CustomTabAvailabilityChecker,
|
||||||
private val customTabHandler: CustomTabHandler,
|
private val customTabHandler: CustomTabHandler,
|
||||||
private val accountProviderDataSource: AccountProviderDataSource,
|
private val accountProviderDataSource: AccountProviderDataSource,
|
||||||
|
private val defaultLoginUserStory: DefaultLoginUserStory,
|
||||||
) : BackstackNode<LoginFlowNode.NavTarget>(
|
) : BackstackNode<LoginFlowNode.NavTarget>(
|
||||||
backstack = BackStack(
|
backstack = BackStack(
|
||||||
initialElement = NavTarget.ConfirmAccountProvider,
|
initialElement = NavTarget.ConfirmAccountProvider,
|
||||||
|
|
@ -75,6 +78,11 @@ class LoginFlowNode @AssistedInject constructor(
|
||||||
|
|
||||||
private val inputs: Inputs = inputs()
|
private val inputs: Inputs = inputs()
|
||||||
|
|
||||||
|
override fun onBuilt() {
|
||||||
|
super.onBuilt()
|
||||||
|
defaultLoginUserStory.setLoginFlowIsDone(false)
|
||||||
|
}
|
||||||
|
|
||||||
sealed interface NavTarget : Parcelable {
|
sealed interface NavTarget : Parcelable {
|
||||||
@Parcelize
|
@Parcelize
|
||||||
object ConfirmAccountProvider : NavTarget
|
object ConfirmAccountProvider : NavTarget
|
||||||
|
|
@ -88,6 +96,9 @@ class LoginFlowNode @AssistedInject constructor(
|
||||||
@Parcelize
|
@Parcelize
|
||||||
object LoginPassword : NavTarget
|
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
|
||||||
}
|
}
|
||||||
|
|
@ -144,12 +155,28 @@ class LoginFlowNode @AssistedInject constructor(
|
||||||
createNode<SearchAccountProviderNode>(buildContext, plugins = listOf(callback))
|
createNode<SearchAccountProviderNode>(buildContext, plugins = listOf(callback))
|
||||||
}
|
}
|
||||||
NavTarget.LoginPassword -> {
|
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 -> {
|
is NavTarget.OidcView -> {
|
||||||
val input = OidcNode.Inputs(navTarget.oidcDetails)
|
val input = OidcNode.Inputs(navTarget.oidcDetails)
|
||||||
createNode<OidcNode>(buildContext, plugins = listOf(input))
|
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.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
|
||||||
|
|
@ -33,13 +34,22 @@ class LoginPasswordNode @AssistedInject constructor(
|
||||||
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()
|
||||||
LoginPasswordView(
|
LoginPasswordView(
|
||||||
state = state,
|
state = state,
|
||||||
modifier = modifier,
|
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.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
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.features.login.impl.accountprovider.AccountProviderDataSource
|
||||||
import io.element.android.libraries.architecture.Async
|
import io.element.android.libraries.architecture.Async
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
|
|
@ -36,6 +37,7 @@ import javax.inject.Inject
|
||||||
class LoginPasswordPresenter @Inject constructor(
|
class LoginPasswordPresenter @Inject constructor(
|
||||||
private val authenticationService: MatrixAuthenticationService,
|
private val authenticationService: MatrixAuthenticationService,
|
||||||
private val accountProviderDataSource: AccountProviderDataSource,
|
private val accountProviderDataSource: AccountProviderDataSource,
|
||||||
|
private val defaultLoginUserStory: DefaultLoginUserStory,
|
||||||
) : Presenter<LoginPasswordState> {
|
) : Presenter<LoginPasswordState> {
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -77,6 +79,8 @@ class LoginPasswordPresenter @Inject constructor(
|
||||||
loggedInState.value = Async.Loading()
|
loggedInState.value = Async.Loading()
|
||||||
authenticationService.login(formState.login.trim(), formState.password)
|
authenticationService.login(formState.login.trim(), formState.password)
|
||||||
.onSuccess { sessionId ->
|
.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)
|
loggedInState.value = Async.Success(sessionId)
|
||||||
}
|
}
|
||||||
.onFailure { failure ->
|
.onFailure { failure ->
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,7 @@ import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
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.Async
|
import io.element.android.libraries.architecture.Async
|
||||||
import io.element.android.libraries.designsystem.ElementTextStyles
|
import io.element.android.libraries.designsystem.ElementTextStyles
|
||||||
|
|
@ -82,8 +83,9 @@ import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
@Composable
|
@Composable
|
||||||
fun LoginPasswordView(
|
fun LoginPasswordView(
|
||||||
state: LoginPasswordState,
|
state: LoginPasswordState,
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
onBackPressed: () -> Unit,
|
onBackPressed: () -> Unit,
|
||||||
|
onWaitListError: (LoginFormState) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val isLoading by remember(state.loginAction) {
|
val isLoading by remember(state.loginAction) {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
|
|
@ -133,7 +135,8 @@ fun LoginPasswordView(
|
||||||
subTitle = stringResource(id = R.string.screen_login_form_header)
|
subTitle = stringResource(id = R.string.screen_login_form_header)
|
||||||
)
|
)
|
||||||
Spacer(Modifier.height(32.dp))
|
Spacer(Modifier.height(32.dp))
|
||||||
LoginForm(state = state,
|
LoginForm(
|
||||||
|
state = state,
|
||||||
isLoading = isLoading,
|
isLoading = isLoading,
|
||||||
onSubmit = ::submit
|
onSubmit = ::submit
|
||||||
)
|
)
|
||||||
|
|
@ -152,9 +155,16 @@ fun LoginPasswordView(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.loginAction is Async.Failure) {
|
if (state.loginAction is Async.Failure) {
|
||||||
LoginErrorDialog(error = state.loginAction.error, onDismiss = {
|
when {
|
||||||
state.eventSink(LoginPasswordEvents.ClearError)
|
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
|
@Composable
|
||||||
internal fun LoginErrorDialog(error: Throwable, onDismiss: () -> Unit) {
|
internal fun LoginErrorDialog(error: Throwable, onDismiss: () -> Unit) {
|
||||||
ErrorDialog(
|
ErrorDialog(
|
||||||
|
title = stringResource(id = CommonStrings.dialog_title_error),
|
||||||
content = stringResource(loginError(error)),
|
content = stringResource(loginError(error)),
|
||||||
onDismiss = onDismiss
|
onDismiss = onDismiss
|
||||||
)
|
)
|
||||||
|
|
@ -288,6 +299,7 @@ internal fun LoginPasswordViewDarkPreview(@PreviewParameter(LoginPasswordStatePr
|
||||||
private fun ContentToPreview(state: LoginPasswordState) {
|
private fun ContentToPreview(state: LoginPasswordState) {
|
||||||
LoginPasswordView(
|
LoginPasswordView(
|
||||||
state = state,
|
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_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_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_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_submit">"Pokračovat"</string>
|
||||||
<string name="screen_change_server_title">"Vyberte svůj server"</string>
|
<string name="screen_change_server_title">"Vyberte svůj server"</string>
|
||||||
<string name="screen_login_password_hint">"Heslo"</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_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_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_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_submit">"Weiter"</string>
|
||||||
<string name="screen_change_server_title">"Wählen deinen Server"</string>
|
<string name="screen_change_server_title">"Wählen deinen Server"</string>
|
||||||
<string name="screen_login_password_hint">"Passwort"</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_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_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_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_submit">"Continuați"</string>
|
||||||
<string name="screen_change_server_title">"Selectați serverul"</string>
|
<string name="screen_change_server_title">"Selectați serverul"</string>
|
||||||
<string name="screen_login_password_hint">"Parola"</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_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_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_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_submit">"Pokračovať"</string>
|
||||||
<string name="screen_change_server_title">"Vyberte svoj server"</string>
|
<string name="screen_change_server_title">"Vyberte svoj server"</string>
|
||||||
<string name="screen_login_password_hint">"Heslo"</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_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_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_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_submit">"Continue"</string>
|
||||||
<string name="screen_change_server_title">"Select your server"</string>
|
<string name="screen_change_server_title">"Select your server"</string>
|
||||||
<string name="screen_login_password_hint">"Password"</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.molecule.moleculeFlow
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth.assertThat
|
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.accountprovider.AccountProviderDataSource
|
||||||
import io.element.android.features.login.impl.util.defaultAccountProvider
|
import io.element.android.features.login.impl.util.defaultAccountProvider
|
||||||
import io.element.android.libraries.architecture.Async
|
import io.element.android.libraries.architecture.Async
|
||||||
|
|
@ -38,9 +39,11 @@ class LoginPasswordPresenterTest {
|
||||||
fun `present - initial state`() = runTest {
|
fun `present - initial state`() = runTest {
|
||||||
val authenticationService = FakeAuthenticationService()
|
val authenticationService = FakeAuthenticationService()
|
||||||
val accountProviderDataSource = AccountProviderDataSource()
|
val accountProviderDataSource = AccountProviderDataSource()
|
||||||
|
val loginUserStory = DefaultLoginUserStory()
|
||||||
val presenter = LoginPasswordPresenter(
|
val presenter = LoginPasswordPresenter(
|
||||||
authenticationService,
|
authenticationService,
|
||||||
accountProviderDataSource,
|
accountProviderDataSource,
|
||||||
|
loginUserStory,
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
|
|
@ -57,9 +60,11 @@ class LoginPasswordPresenterTest {
|
||||||
fun `present - enter login and password`() = runTest {
|
fun `present - enter login and password`() = runTest {
|
||||||
val authenticationService = FakeAuthenticationService()
|
val authenticationService = FakeAuthenticationService()
|
||||||
val accountProviderDataSource = AccountProviderDataSource()
|
val accountProviderDataSource = AccountProviderDataSource()
|
||||||
|
val loginUserStory = DefaultLoginUserStory()
|
||||||
val presenter = LoginPasswordPresenter(
|
val presenter = LoginPasswordPresenter(
|
||||||
authenticationService,
|
authenticationService,
|
||||||
accountProviderDataSource,
|
accountProviderDataSource,
|
||||||
|
loginUserStory,
|
||||||
)
|
)
|
||||||
authenticationService.givenHomeserver(A_HOMESERVER)
|
authenticationService.givenHomeserver(A_HOMESERVER)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
|
|
@ -81,14 +86,17 @@ class LoginPasswordPresenterTest {
|
||||||
fun `present - submit`() = runTest {
|
fun `present - submit`() = runTest {
|
||||||
val authenticationService = FakeAuthenticationService()
|
val authenticationService = FakeAuthenticationService()
|
||||||
val accountProviderDataSource = AccountProviderDataSource()
|
val accountProviderDataSource = AccountProviderDataSource()
|
||||||
|
val loginUserStory = DefaultLoginUserStory().apply { setLoginFlowIsDone(false) }
|
||||||
val presenter = LoginPasswordPresenter(
|
val presenter = LoginPasswordPresenter(
|
||||||
authenticationService,
|
authenticationService,
|
||||||
accountProviderDataSource,
|
accountProviderDataSource,
|
||||||
|
loginUserStory,
|
||||||
)
|
)
|
||||||
authenticationService.givenHomeserver(A_HOMESERVER)
|
authenticationService.givenHomeserver(A_HOMESERVER)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
assertThat(loginUserStory.loginFlowIsDone.value).isFalse()
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
initialState.eventSink.invoke(LoginPasswordEvents.SetLogin(A_USER_NAME))
|
initialState.eventSink.invoke(LoginPasswordEvents.SetLogin(A_USER_NAME))
|
||||||
initialState.eventSink.invoke(LoginPasswordEvents.SetPassword(A_PASSWORD))
|
initialState.eventSink.invoke(LoginPasswordEvents.SetPassword(A_PASSWORD))
|
||||||
|
|
@ -99,6 +107,7 @@ class LoginPasswordPresenterTest {
|
||||||
assertThat(submitState.loginAction).isInstanceOf(Async.Loading::class.java)
|
assertThat(submitState.loginAction).isInstanceOf(Async.Loading::class.java)
|
||||||
val loggedInState = awaitItem()
|
val loggedInState = awaitItem()
|
||||||
assertThat(loggedInState.loginAction).isEqualTo(Async.Success(A_SESSION_ID))
|
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 {
|
fun `present - submit with error`() = runTest {
|
||||||
val authenticationService = FakeAuthenticationService()
|
val authenticationService = FakeAuthenticationService()
|
||||||
val accountProviderDataSource = AccountProviderDataSource()
|
val accountProviderDataSource = AccountProviderDataSource()
|
||||||
|
val loginUserStory = DefaultLoginUserStory()
|
||||||
val presenter = LoginPasswordPresenter(
|
val presenter = LoginPasswordPresenter(
|
||||||
authenticationService,
|
authenticationService,
|
||||||
accountProviderDataSource,
|
accountProviderDataSource,
|
||||||
|
loginUserStory,
|
||||||
)
|
)
|
||||||
authenticationService.givenHomeserver(A_HOMESERVER)
|
authenticationService.givenHomeserver(A_HOMESERVER)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
|
|
@ -132,9 +143,11 @@ class LoginPasswordPresenterTest {
|
||||||
fun `present - clear error`() = runTest {
|
fun `present - clear error`() = runTest {
|
||||||
val authenticationService = FakeAuthenticationService()
|
val authenticationService = FakeAuthenticationService()
|
||||||
val accountProviderDataSource = AccountProviderDataSource()
|
val accountProviderDataSource = AccountProviderDataSource()
|
||||||
|
val loginUserStory = DefaultLoginUserStory()
|
||||||
val presenter = LoginPasswordPresenter(
|
val presenter = LoginPasswordPresenter(
|
||||||
authenticationService,
|
authenticationService,
|
||||||
accountProviderDataSource,
|
accountProviderDataSource,
|
||||||
|
loginUserStory,
|
||||||
)
|
)
|
||||||
authenticationService.givenHomeserver(A_HOMESERVER)
|
authenticationService.givenHomeserver(A_HOMESERVER)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
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_open_osm_maps">"Otevřít v OpenStreetMap"</string>
|
||||||
<string name="screen_share_this_location_action">"Sdílet tuto polohu"</string>
|
<string name="screen_share_this_location_action">"Sdílet tuto polohu"</string>
|
||||||
<string name="screen_view_location_title">"Poloha"</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">"Rageshake"</string>
|
||||||
<string name="settings_rageshake_detection_threshold">"Práh detekce"</string>
|
<string name="settings_rageshake_detection_threshold">"Práh detekce"</string>
|
||||||
<string name="settings_title_general">"Obecné"</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_open_osm_maps">"In OpenStreetMap öffnen"</string>
|
||||||
<string name="screen_share_this_location_action">"Diesen Ort teilen"</string>
|
<string name="screen_share_this_location_action">"Diesen Ort teilen"</string>
|
||||||
<string name="screen_view_location_title">"Standort"</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">"Rageshake"</string>
|
||||||
<string name="settings_rageshake_detection_threshold">"Erkennungsschwelle"</string>
|
<string name="settings_rageshake_detection_threshold">"Erkennungsschwelle"</string>
|
||||||
<string name="settings_title_general">"Allgemein"</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_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_share_this_location_action">"Distribuiți această locație"</string>
|
||||||
<string name="screen_view_location_title">"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">"Rageshake"</string>
|
||||||
<string name="settings_rageshake_detection_threshold">"Prag de detecție"</string>
|
<string name="settings_rageshake_detection_threshold">"Prag de detecție"</string>
|
||||||
<string name="settings_title_general">"General"</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_open_osm_maps">"Otvoriť v OpenStreetMap"</string>
|
||||||
<string name="screen_share_this_location_action">"Zdieľajte túto polohu"</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_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">"Zúrivé potrasenie"</string>
|
||||||
<string name="settings_rageshake_detection_threshold">"Prahová hodnota detekcie"</string>
|
<string name="settings_rageshake_detection_threshold">"Prahová hodnota detekcie"</string>
|
||||||
<string name="settings_title_general">"Všeobecné"</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_open_osm_maps">"Open in OpenStreetMap"</string>
|
||||||
<string name="screen_share_this_location_action">"Share this location"</string>
|
<string name="screen_share_this_location_action">"Share this location"</string>
|
||||||
<string name="screen_view_location_title">"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">"Rageshake"</string>
|
||||||
<string name="settings_rageshake_detection_threshold">"Detection threshold"</string>
|
<string name="settings_rageshake_detection_threshold">"Detection threshold"</string>
|
||||||
<string name="settings_title_general">"General"</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.LaunchedEffect
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
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.accountprovider.AccountProviderDataSource
|
||||||
import io.element.android.features.login.impl.screens.loginpassword.LoginPasswordPresenter
|
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.screens.loginpassword.LoginPasswordView
|
||||||
|
|
@ -33,7 +34,8 @@ class LoginScreen(private val authenticationService: MatrixAuthenticationService
|
||||||
val presenter = remember {
|
val presenter = remember {
|
||||||
LoginPasswordPresenter(
|
LoginPasswordPresenter(
|
||||||
authenticationService = authenticationService,
|
authenticationService = authenticationService,
|
||||||
AccountProviderDataSource()
|
AccountProviderDataSource(),
|
||||||
|
DefaultLoginUserStory(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -46,6 +48,7 @@ class LoginScreen(private val authenticationService: MatrixAuthenticationService
|
||||||
state = state,
|
state = state,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
onBackPressed = {},
|
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_server_confirmation_.*",
|
||||||
"screen_change_server_.*",
|
"screen_change_server_.*",
|
||||||
"screen_change_account_provider_.*",
|
"screen_change_account_provider_.*",
|
||||||
"screen_account_provider_.*"
|
"screen_account_provider_.*",
|
||||||
|
"screen_waitlist_.*"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue