Repository: element-hq/element-x-android (language: kotlin)

Issue:
Waiting list screen - Implement `IO_ELEMENT_X_WAIT_LIST` error code on `/login`
As a user, I want to understand why I cannot login and how I can be part of this allow list defined on my homeserver.

iOS PRs:
- https://github.com/vector-im/element-x-ios/pull/1165
- https://github.com/vector-im/element-x-ios/pull/1183
- Latest design: https://github.com/vector-im/element-x-ios/pull/1224



Discussion hints:
nan

Pull request: Implement waiting list screen (error IO_ELEMENT_X_WAIT_LIST)
Closes #768 

Only the first screen is used, and the "You're in" screen are displayed (no "setting up your account" screen, which may be handled later, and anyway not in the `:login` module).

Dark and light are identical (so we have some hard-coded colors), only the dialog colors may change.

## Demo:

|Without "You're in" (previous demo)|You're in demo (+ dark)|
|-|-|
|![WaitList](https://github.com/vector-im/element-x-android/assets/3940906/7e6fd7b0-56f4-4533-ab74-a8e4ec356aa6)|![WaitListDarkYoureIn](https://github.com/vector-im/element-x-android/assets/3940906/399232af-f5f1-4aeb-b56c-c331c9ceecf2)|

Note: since we are using a test account here, there is no way to make it successfully login. But at each attempt, if the login is successful, the regular flow will continue: analytics optin then room list. Tested OK locally.

## Screenshot

|Portrait|Landscape|
|-|-|
|<img width="268" alt="image" src="https://github.com/vector-im/element-x-android/assets/3940906/2cb1c74f-3b14-4997-985b-2c2b119471a8">|<img width="519" alt="image" src="https://github.com/vector-im/element-x-android/assets/3940906/385fc466-c623-4b2b-a987-f688e3c7e1ce">|
|<img width="244" alt="image" src="https://github.com/vector-im/element-x-android/assets/3940906/09a4b1cd-68bc-4063-8ae7-eb3aa17f1e92">|<img width="484" alt="image" src="https://github.com/vector-im/element-x-android/assets/3940906/1b85e680-8554-4d57-b7f9-1ad6dfe2b4c7">|

## Tests

### App not logged in

- Login using `m1-element` account. I can provide the password if necessary. This account will be rejected. There is no way to go further using this account, but the code can be hacked to use different credentials when attempting login again, so that we can see the "You're in" screen. Note: if the app is killed once the You're in screen is displayed, restarting it shows the room list.
- Login using any another account: the wait screen is not displayed and the room list is displayed (acually the analytics screen first)

### App is logged in

- cold start show the room list

Test command: ./gradlew :features:login:impl:testDebugUnitTest --tests "io.element.android.features.login.impl.screens.loginpassword.LoginPasswordPresenterTest" --tests "io.element.android.features.login.impl.screens.waitlistscreen.WaitListPresenterTest"

Fix patch:
diff --git a/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt
index 733485fcef0..ba01d152976 100644
--- a/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt
+++ b/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt
@@ -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))
     }
diff --git a/features/login/api/src/main/kotlin/io/element/android/features/login/api/LoginUserStory.kt b/features/login/api/src/main/kotlin/io/element/android/features/login/api/LoginUserStory.kt
new file mode 100644
index 00000000000..3a4cc545632
--- /dev/null
+++ b/features/login/api/src/main/kotlin/io/element/android/features/login/api/LoginUserStory.kt
@@ -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>
+}
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/DefaultLoginUserStory.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/DefaultLoginUserStory.kt
new file mode 100644
index 00000000000..26b00068bc4
--- /dev/null
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/DefaultLoginUserStory.kt
@@ -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
+    }
+}
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/LoginFlowNode.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/LoginFlowNode.kt
index f8d965ec6de..fe47fb1b675 100644
--- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/LoginFlowNode.kt
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/LoginFlowNode.kt
@@ -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))
+            }
         }
     }
 
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/error/WaitListError.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/error/WaitListError.kt
new file mode 100644
index 00000000000..99060f34644
--- /dev/null
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/error/WaitListError.kt
@@ -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()
+}
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordNode.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordNode.kt
index 630b08570c4..cb5542d2eb3 100644
--- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordNode.kt
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordNode.kt
@@ -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,
         )
     }
 }
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordPresenter.kt
index 1fc4a10bbb0..b2ea5fb985c 100644
--- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordPresenter.kt
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordPresenter.kt
@@ -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 ->
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordView.kt
index 9d6fe7d1eab..e209a9af552 100644
--- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordView.kt
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordView.kt
@@ -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 = {},
     )
 }
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/waitlistscreen/WaitListEvents.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/waitlistscreen/WaitListEvents.kt
new file mode 100644
index 00000000000..5ceee99f91d
--- /dev/null
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/waitlistscreen/WaitListEvents.kt
@@ -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
+}
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/waitlistscreen/WaitListNode.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/waitlistscreen/WaitListNode.kt
new file mode 100644
index 00000000000..24b5f271a05
--- /dev/null
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/waitlistscreen/WaitListNode.kt
@@ -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
+        )
+    }
+}
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/waitlistscreen/WaitListPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/waitlistscreen/WaitListPresenter.kt
new file mode 100644
index 00000000000..9c07204ab29
--- /dev/null
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/waitlistscreen/WaitListPresenter.kt
@@ -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)
+            }
+    }
+}
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/waitlistscreen/WaitListState.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/waitlistscreen/WaitListState.kt
new file mode 100644
index 00000000000..f50de7e194b
--- /dev/null
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/waitlistscreen/WaitListState.kt
@@ -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
+)
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/waitlistscreen/WaitListStateProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/waitlistscreen/WaitListStateProvider.kt
new file mode 100644
index 00000000000..5907ff1acf7
--- /dev/null
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/waitlistscreen/WaitListStateProvider.kt
@@ -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 = {}
+)
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/waitlistscreen/WaitListView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/waitlistscreen/WaitListView.kt
new file mode 100644
index 00000000000..15105a80489
--- /dev/null
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/waitlistscreen/WaitListView.kt
@@ -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 = {},
+    )
+}
diff --git a/features/login/impl/src/main/res/drawable/light_dark.png b/features/login/impl/src/main/res/drawable/light_dark.png
new file mode 100644
index 00000000000..2ff7516878b
Binary files /dev/null and b/features/login/impl/src/main/res/drawable/light_dark.png differ
diff --git a/features/login/impl/src/main/res/values-cs/translations.xml b/features/login/impl/src/main/res/values-cs/translations.xml
index 300d8516931..199b2a0b5c8 100644
--- a/features/login/impl/src/main/res/values-cs/translations.xml
+++ b/features/login/impl/src/main/res/values-cs/translations.xml
@@ -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>
diff --git a/features/login/impl/src/main/res/values-de/translations.xml b/features/login/impl/src/main/res/values-de/translations.xml
index a79ddd1c087..5bedf6e5d42 100644
--- a/features/login/impl/src/main/res/values-de/translations.xml
+++ b/features/login/impl/src/main/res/values-de/translations.xml
@@ -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>
diff --git a/features/login/impl/src/main/res/values-ro/translations.xml b/features/login/impl/src/main/res/values-ro/translations.xml
index 70651bc53b9..08008772b80 100644
--- a/features/login/impl/src/main/res/values-ro/translations.xml
+++ b/features/login/impl/src/main/res/values-ro/translations.xml
@@ -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>
diff --git a/features/login/impl/src/main/res/values-sk/translations.xml b/features/login/impl/src/main/res/values-sk/translations.xml
index c593e1df54b..e90df39d515 100644
--- a/features/login/impl/src/main/res/values-sk/translations.xml
+++ b/features/login/impl/src/main/res/values-sk/translations.xml
@@ -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>
diff --git a/features/login/impl/src/main/res/values/localazy.xml b/features/login/impl/src/main/res/values/localazy.xml
index 55324613edb..b7b2c2a1eba 100644
--- a/features/login/impl/src/main/res/values/localazy.xml
+++ b/features/login/impl/src/main/res/values/localazy.xml
@@ -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>
diff --git a/libraries/ui-strings/src/main/res/values-cs/translations.xml b/libraries/ui-strings/src/main/res/values-cs/translations.xml
index 161acdff389..9d897db9d8c 100644
--- a/libraries/ui-strings/src/main/res/values-cs/translations.xml
+++ b/libraries/ui-strings/src/main/res/values-cs/translations.xml
@@ -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>
diff --git a/libraries/ui-strings/src/main/res/values-de/translations.xml b/libraries/ui-strings/src/main/res/values-de/translations.xml
index be70035c991..bb469ad0b19 100644
--- a/libraries/ui-strings/src/main/res/values-de/translations.xml
+++ b/libraries/ui-strings/src/main/res/values-de/translations.xml
@@ -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>
diff --git a/libraries/ui-strings/src/main/res/values-ro/translations.xml b/libraries/ui-strings/src/main/res/values-ro/translations.xml
index e0e700720c9..d90a7a6cfc3 100644
--- a/libraries/ui-strings/src/main/res/values-ro/translations.xml
+++ b/libraries/ui-strings/src/main/res/values-ro/translations.xml
@@ -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>
diff --git a/libraries/ui-strings/src/main/res/values-sk/translations.xml b/libraries/ui-strings/src/main/res/values-sk/translations.xml
index 607b3aeca9c..707032b2379 100644
--- a/libraries/ui-strings/src/main/res/values-sk/translations.xml
+++ b/libraries/ui-strings/src/main/res/values-sk/translations.xml
@@ -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>
diff --git a/libraries/ui-strings/src/main/res/values/localazy.xml b/libraries/ui-strings/src/main/res/values/localazy.xml
index 1a48a00c5a7..7558bd30b3c 100644
--- a/libraries/ui-strings/src/main/res/values/localazy.xml
+++ b/libraries/ui-strings/src/main/res/values/localazy.xml
@@ -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>
diff --git a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/LoginScreen.kt b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/LoginScreen.kt
index 54bef4652bf..ec4d7cf9f26 100644
--- a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/LoginScreen.kt
+++ b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/LoginScreen.kt
@@ -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 = {},
         )
     }
 }
diff --git a/tools/localazy/config.json b/tools/localazy/config.json
index a7317af0db4..3feadf7a7ad 100644
--- a/tools/localazy/config.json
+++ b/tools/localazy/config.json
@@ -76,7 +76,8 @@
         "screen_server_confirmation_.*",
         "screen_change_server_.*",
         "screen_change_account_provider_.*",
-        "screen_account_provider_.*"
+        "screen_account_provider_.*",
+        "screen_waitlist_.*"
       ]
     },
     {


Test patch:
diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordPresenterTest.kt
index c4c8a971556..afd4b542e41 100644
--- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordPresenterTest.kt
+++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordPresenterTest.kt
@@ -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) {
diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/waitlistscreen/WaitListPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/waitlistscreen/WaitListPresenterTest.kt
new file mode 100644
index 00000000000..389ac521769
--- /dev/null
+++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/waitlistscreen/WaitListPresenterTest.kt
@@ -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()
+        }
+    }
+}
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.screens.waitlistscreen_null_DefaultGroup_WaitListViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.screens.waitlistscreen_null_DefaultGroup_WaitListViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png
new file mode 100644
index 00000000000..6b5e4c405f2
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.screens.waitlistscreen_null_DefaultGroup_WaitListViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:e97d88bef72c332cd145dc1080a989d163478c0252a9993f2f474bd51f2e4da8
+size 148762
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.screens.waitlistscreen_null_DefaultGroup_WaitListViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.screens.waitlistscreen_null_DefaultGroup_WaitListViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png
new file mode 100644
index 00000000000..151e09cfc0b
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.screens.waitlistscreen_null_DefaultGroup_WaitListViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:e8edd6d72db9efaaed76ac64f9882a5b66ac30747355815955157a3f3fc98c2c
+size 149344
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.screens.waitlistscreen_null_DefaultGroup_WaitListViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.screens.waitlistscreen_null_DefaultGroup_WaitListViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png
new file mode 100644
index 00000000000..d1642cd7417
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.screens.waitlistscreen_null_DefaultGroup_WaitListViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:e099a30e3090ffd9c0f30e095131cb60d53b615623acd50565bde8fa28e7fe74
+size 62810
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.screens.waitlistscreen_null_DefaultGroup_WaitListViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.screens.waitlistscreen_null_DefaultGroup_WaitListViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png
new file mode 100644
index 00000000000..6b5e4c405f2
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.screens.waitlistscreen_null_DefaultGroup_WaitListViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:e97d88bef72c332cd145dc1080a989d163478c0252a9993f2f474bd51f2e4da8
+size 148762
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.screens.waitlistscreen_null_DefaultGroup_WaitListViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.screens.waitlistscreen_null_DefaultGroup_WaitListViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png
new file mode 100644
index 00000000000..822450c8af3
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.screens.waitlistscreen_null_DefaultGroup_WaitListViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:85222447b700315e6eea458fc72d61fb741018cb80c3a4530d4efc08ac9335ac
+size 129373
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.screens.waitlistscreen_null_DefaultGroup_WaitListViewLightPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.screens.waitlistscreen_null_DefaultGroup_WaitListViewLightPreview_0_null_0,NEXUS_5,1.0,en].png
new file mode 100644
index 00000000000..6b5e4c405f2
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.screens.waitlistscreen_null_DefaultGroup_WaitListViewLightPreview_0_null_0,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:e97d88bef72c332cd145dc1080a989d163478c0252a9993f2f474bd51f2e4da8
+size 148762
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.screens.waitlistscreen_null_DefaultGroup_WaitListViewLightPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.screens.waitlistscreen_null_DefaultGroup_WaitListViewLightPreview_0_null_1,NEXUS_5,1.0,en].png
new file mode 100644
index 00000000000..151e09cfc0b
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.screens.waitlistscreen_null_DefaultGroup_WaitListViewLightPreview_0_null_1,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:e8edd6d72db9efaaed76ac64f9882a5b66ac30747355815955157a3f3fc98c2c
+size 149344
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.screens.waitlistscreen_null_DefaultGroup_WaitListViewLightPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.screens.waitlistscreen_null_DefaultGroup_WaitListViewLightPreview_0_null_2,NEXUS_5,1.0,en].png
new file mode 100644
index 00000000000..dfa30f1f08e
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.screens.waitlistscreen_null_DefaultGroup_WaitListViewLightPreview_0_null_2,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:71289cbba79500c063ec80430fb46bc544ccbe6fa7035ac81c81322c9f1a300f
+size 63631
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.screens.waitlistscreen_null_DefaultGroup_WaitListViewLightPreview_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.screens.waitlistscreen_null_DefaultGroup_WaitListViewLightPreview_0_null_3,NEXUS_5,1.0,en].png
new file mode 100644
index 00000000000..6b5e4c405f2
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.screens.waitlistscreen_null_DefaultGroup_WaitListViewLightPreview_0_null_3,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:e97d88bef72c332cd145dc1080a989d163478c0252a9993f2f474bd51f2e4da8
+size 148762
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.screens.waitlistscreen_null_DefaultGroup_WaitListViewLightPreview_0_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.screens.waitlistscreen_null_DefaultGroup_WaitListViewLightPreview_0_null_4,NEXUS_5,1.0,en].png
new file mode 100644
index 00000000000..822450c8af3
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.screens.waitlistscreen_null_DefaultGroup_WaitListViewLightPreview_0_null_4,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:85222447b700315e6eea458fc72d61fb741018cb80c3a4530d4efc08ac9335ac
+size 129373