{
+ return buildList {
+ if (data.contains(".")) {
+ // TLD detected?
+ } else {
+ add("${data}.org")
+ add("${data}.com")
+ add("${data}.io")
+ }
+ // Always try what the user has entered
+ add(data)
+ }
+ }
+}
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/network/DefaultWellknownRequest.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/network/DefaultWellknownRequest.kt
new file mode 100644
index 0000000000..7de6d26f10
--- /dev/null
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/network/DefaultWellknownRequest.kt
@@ -0,0 +1,36 @@
+/*
+ * 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.resolver.network
+
+import com.squareup.anvil.annotations.ContributesBinding
+import io.element.android.libraries.di.AppScope
+import io.element.android.libraries.network.RetrofitFactory
+import javax.inject.Inject
+
+@ContributesBinding(AppScope::class)
+class DefaultWellknownRequest @Inject constructor(
+ private val retrofitFactory: RetrofitFactory,
+) : WellknownRequest {
+ /**
+ * Return the WellKnown data, if found.
+ * @param baseUrl for instance https://matrix.org
+ */
+ override suspend fun execute(baseUrl: String): WellKnown {
+ val wellknownApi = retrofitFactory.create(baseUrl)
+ .create(WellknownAPI::class.java)
+ return wellknownApi.getWellKnown()
+ }
+}
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/network/WellKnown.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/network/WellKnown.kt
new file mode 100644
index 0000000000..63b6e7d189
--- /dev/null
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/network/WellKnown.kt
@@ -0,0 +1,58 @@
+/*
+ * 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.resolver.network
+
+import io.element.android.libraries.core.bool.orFalse
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+/**
+ * https://matrix.org/docs/spec/client_server/r0.4.0.html#server-discovery
+ *
+ * {
+ * "m.homeserver": {
+ * "base_url": "https://matrix.org"
+ * },
+ * "m.identity_server": {
+ * "base_url": "https://vector.im"
+ * },
+ * "org.matrix.msc3575.proxy": {
+ * "url": "https://slidingsync.lab.matrix.org"
+ * }
+ * }
+ *
+ * .
+ */
+@Serializable
+data class WellKnown(
+ @SerialName("m.homeserver")
+ val homeServer: WellKnownBaseConfig? = null,
+
+ @SerialName("m.identity_server")
+ val identityServer: WellKnownBaseConfig? = null,
+
+ @SerialName("org.matrix.msc3575.proxy")
+ val slidingSyncProxy: WellKnownSlidingSyncConfig? = null,
+) {
+ fun isValid(): Boolean {
+ return homeServer?.baseURL?.isNotBlank().orFalse()
+ }
+
+ fun supportSlidingSync(): Boolean {
+ return slidingSyncProxy?.url?.isNotBlank().orFalse()
+ }
+}
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/network/WellKnownBaseConfig.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/network/WellKnownBaseConfig.kt
new file mode 100644
index 0000000000..87b86736fa
--- /dev/null
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/network/WellKnownBaseConfig.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.resolver.network
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+/**
+ * https://matrix.org/docs/spec/client_server/r0.4.0.html#server-discovery
+ *
+ * {
+ * "base_url": "https://element.io"
+ * }
+ *
+ * .
+ */
+@Serializable
+data class WellKnownBaseConfig(
+ @SerialName("base_url")
+ val baseURL: String? = null
+)
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/network/WellKnownSlidingSyncConfig.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/network/WellKnownSlidingSyncConfig.kt
new file mode 100644
index 0000000000..98c712d9ac
--- /dev/null
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/network/WellKnownSlidingSyncConfig.kt
@@ -0,0 +1,26 @@
+/*
+ * 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.resolver.network
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class WellKnownSlidingSyncConfig(
+ @SerialName("url")
+ val url: String? = null,
+)
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootEvents.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/network/WellknownAPI.kt
similarity index 63%
rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootEvents.kt
rename to features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/network/WellknownAPI.kt
index 5aa5071876..04a0dfb803 100644
--- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootEvents.kt
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/network/WellknownAPI.kt
@@ -14,12 +14,11 @@
* limitations under the License.
*/
-package io.element.android.features.login.impl.root
+package io.element.android.features.login.impl.resolver.network
-sealed interface LoginRootEvents {
- object RetryFetchServerInfo : LoginRootEvents
- data class SetLogin(val login: String) : LoginRootEvents
- data class SetPassword(val password: String) : LoginRootEvents
- object Submit : LoginRootEvents
- object ClearError : LoginRootEvents
+import retrofit2.http.GET
+
+internal interface WellknownAPI {
+ @GET(".well-known/matrix/client")
+ suspend fun getWellKnown(): WellKnown
}
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/network/WellknownRequest.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/network/WellknownRequest.kt
new file mode 100644
index 0000000000..570b621b83
--- /dev/null
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/network/WellknownRequest.kt
@@ -0,0 +1,24 @@
+/*
+ * 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.resolver.network
+
+interface WellknownRequest {
+ /**
+ * Return the WellKnown data, or throw an error if not found.
+ * @param baseUrl for instance https://matrix.org
+ */
+ suspend fun execute(baseUrl: String): WellKnown
+}
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootPresenter.kt
deleted file mode 100644
index f55c2030e7..0000000000
--- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootPresenter.kt
+++ /dev/null
@@ -1,171 +0,0 @@
-/*
- * Copyright (c) 2023 New Vector Ltd
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package io.element.android.features.login.impl.root
-
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.MutableState
-import androidx.compose.runtime.collectAsState
-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.api.oidc.OidcAction
-import io.element.android.features.login.impl.oidc.customtab.DefaultOidcActionFlow
-import io.element.android.features.login.impl.util.LoginConstants
-import io.element.android.libraries.architecture.Async
-import io.element.android.libraries.architecture.Presenter
-import io.element.android.libraries.architecture.execute
-import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
-import io.element.android.libraries.matrix.api.auth.MatrixHomeServerDetails
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.launch
-import javax.inject.Inject
-
-class LoginRootPresenter @Inject constructor(
- private val authenticationService: MatrixAuthenticationService,
- private val defaultOidcActionFlow: DefaultOidcActionFlow,
-) : Presenter {
-
- @Composable
- override fun present(): LoginRootState {
- val localCoroutineScope = rememberCoroutineScope()
- val currentHomeServerDetails = authenticationService.getHomeserverDetails().collectAsState().value
- val homeserver = currentHomeServerDetails?.url ?: LoginConstants.DEFAULT_HOMESERVER_URL
- val getHomeServerDetailsAction: MutableState> = remember {
- if (currentHomeServerDetails != null) {
- mutableStateOf(Async.Success(currentHomeServerDetails))
- } else {
- mutableStateOf(Async.Uninitialized)
- }
- }
-
- LaunchedEffect(Unit) {
- if (currentHomeServerDetails == null) {
- getHomeServerDetails(homeserver, getHomeServerDetailsAction)
- }
- }
-
- val loggedInState: MutableState = remember {
- mutableStateOf(LoggedInState.NotLoggedIn)
- }
- val formState = rememberSaveable {
- mutableStateOf(LoginFormState.Default)
- }
-
- LaunchedEffect(Unit) {
- launch {
- defaultOidcActionFlow.collect {
- onOidcAction(it, loggedInState)
- }
- }
- }
-
- fun handleEvents(event: LoginRootEvents) {
- when (event) {
- LoginRootEvents.RetryFetchServerInfo -> localCoroutineScope.getHomeServerDetails(homeserver, getHomeServerDetailsAction)
- is LoginRootEvents.SetLogin -> updateFormState(formState) {
- copy(login = event.login)
- }
- is LoginRootEvents.SetPassword -> updateFormState(formState) {
- copy(password = event.password)
- }
- LoginRootEvents.Submit -> {
- val homeServerDetails = getHomeServerDetailsAction.value.dataOrNull() ?: return
- when {
- homeServerDetails.supportsOidcLogin -> localCoroutineScope.submitOidc(loggedInState)
- homeServerDetails.supportsPasswordLogin -> localCoroutineScope.submit(formState.value, loggedInState)
- }
- }
- LoginRootEvents.ClearError -> loggedInState.value = LoggedInState.NotLoggedIn
- }
- }
-
- return LoginRootState(
- homeserverUrl = homeserver,
- homeserverDetails = getHomeServerDetailsAction.value,
- loggedInState = loggedInState.value,
- formState = formState.value,
- eventSink = ::handleEvents
- )
- }
-
- private fun CoroutineScope.getHomeServerDetails(
- homeserver: String,
- state: MutableState>,
- ) = launch {
- suspend {
- authenticationService.setHomeserver(homeserver)
- .map {
- authenticationService.getHomeserverDetails().value!!
- }
- .getOrThrow()
- }.execute(state)
- }
-
- private fun CoroutineScope.submitOidc(loggedInState: MutableState) = launch {
- loggedInState.value = LoggedInState.LoggingIn
- authenticationService.getOidcUrl()
- .onSuccess {
- loggedInState.value = LoggedInState.OidcStarted(it)
- }
- .onFailure { failure ->
- loggedInState.value = LoggedInState.ErrorLoggingIn(failure)
- }
- }
-
- private fun CoroutineScope.submit(formState: LoginFormState, loggedInState: MutableState) = launch {
- loggedInState.value = LoggedInState.LoggingIn
- authenticationService.login(formState.login.trim(), formState.password)
- .onSuccess { sessionId ->
- loggedInState.value = LoggedInState.LoggedIn(sessionId)
- }
- .onFailure { failure ->
- loggedInState.value = LoggedInState.ErrorLoggingIn(failure)
- }
- }
-
- private fun updateFormState(formState: MutableState, updateLambda: LoginFormState.() -> LoginFormState) {
- formState.value = updateLambda(formState.value)
- }
-
- private suspend fun onOidcAction(oidcAction: OidcAction?, loggedInState: MutableState) {
- oidcAction ?: return
- loggedInState.value = LoggedInState.LoggingIn
- when (oidcAction) {
- OidcAction.GoBack -> {
- authenticationService.cancelOidcLogin()
- .onSuccess {
- loggedInState.value = LoggedInState.NotLoggedIn
- }
- .onFailure { failure ->
- loggedInState.value = LoggedInState.ErrorLoggingIn(failure)
- }
- }
- is OidcAction.Success -> {
- authenticationService.loginWithOidc(oidcAction.url)
- .onSuccess { sessionId ->
- loggedInState.value = LoggedInState.LoggedIn(sessionId)
- }
- .onFailure { failure ->
- loggedInState.value = LoggedInState.ErrorLoggingIn(failure)
- }
- }
- }
- defaultOidcActionFlow.reset()
- }
-}
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootStateProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootStateProvider.kt
deleted file mode 100644
index 5f6d7c1f3a..0000000000
--- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootStateProvider.kt
+++ /dev/null
@@ -1,76 +0,0 @@
-/*
- * Copyright (c) 2023 New Vector Ltd
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package io.element.android.features.login.impl.root
-
-import androidx.compose.ui.tooling.preview.PreviewParameterProvider
-import io.element.android.libraries.architecture.Async
-import io.element.android.libraries.matrix.api.auth.MatrixHomeServerDetails
-import io.element.android.libraries.matrix.api.core.SessionId
-
-open class LoginRootStateProvider : PreviewParameterProvider {
- override val values: Sequence
- get() = sequenceOf(
- aLoginRootState(),
- aLoginRootState().copy(
- homeserverDetails = Async.Success(
- MatrixHomeServerDetails(
- "some-custom-server.com",
- supportsPasswordLogin = true,
- supportsOidcLogin = false
- )
- )
- ),
- aLoginRootState().copy(formState = LoginFormState("user", "pass")),
- aLoginRootState().copy(formState = LoginFormState("user", "pass"), loggedInState = LoggedInState.LoggingIn),
- aLoginRootState().copy(formState = LoginFormState("user", "pass"), loggedInState = LoggedInState.ErrorLoggingIn(Throwable())),
- aLoginRootState().copy(formState = LoginFormState("user", "pass"), loggedInState = LoggedInState.LoggedIn(SessionId("@user:domain"))),
- // Oidc
- aLoginRootState().copy(
- homeserverUrl = "server-with-oidc.org",
- homeserverDetails = Async.Success(
- MatrixHomeServerDetails(
- "server-with-oidc.org",
- supportsPasswordLogin = false,
- supportsOidcLogin = true
- )
- )
- ),
- // No password, no oidc support
- aLoginRootState().copy(
- homeserverUrl = "wrong.org",
- homeserverDetails = Async.Success(
- MatrixHomeServerDetails(
- "wrong.org",
- supportsPasswordLogin = false,
- supportsOidcLogin = false
- )
- )
- ),
- // Loading
- aLoginRootState().copy(homeserverDetails = Async.Loading()),
- //Error
- aLoginRootState().copy(homeserverDetails = Async.Failure(Exception("An error occurred"))),
- )
-}
-
-fun aLoginRootState() = LoginRootState(
- homeserverUrl = "matrix.org",
- homeserverDetails = Async.Success(MatrixHomeServerDetails("matrix.org", supportsPasswordLogin = true, supportsOidcLogin = false)),
- loggedInState = LoggedInState.NotLoggedIn,
- formState = LoginFormState.Default,
- eventSink = {}
-)
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerNode.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderNode.kt
similarity index 66%
rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerNode.kt
rename to features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderNode.kt
index 11952ce5a8..45bf4489b5 100644
--- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerNode.kt
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderNode.kt
@@ -14,49 +14,52 @@
* limitations under the License.
*/
-package io.element.android.features.login.impl.changeserver
+package io.element.android.features.login.impl.screens.changeaccountprovider
-import android.content.Context
-import android.content.Intent
-import android.net.Uri
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
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.util.LoginConstants
-import io.element.android.libraries.core.data.tryOrNull
+import io.element.android.features.login.impl.util.openLearnMorePage
import io.element.android.libraries.di.AppScope
@ContributesNode(AppScope::class)
-class ChangeServerNode @AssistedInject constructor(
+class ChangeAccountProviderNode @AssistedInject constructor(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
- private val presenter: ChangeServerPresenter,
+ private val presenter: ChangeAccountProviderPresenter,
) : Node(buildContext, plugins = plugins) {
- private fun onSuccess() {
- navigateUp()
+
+ interface Callback : Plugin {
+ fun onDone()
+ fun onOtherClicked()
}
- private fun openLearnMorePage(context: Context) {
- val intent = Intent(Intent.ACTION_VIEW, Uri.parse(LoginConstants.SLIDING_SYNC_READ_MORE_URL))
- tryOrNull { context.startActivity(intent) }
+ private fun onDone() {
+ plugins().forEach { it.onDone() }
+ }
+
+ private fun onOtherClicked() {
+ plugins().forEach { it.onOtherClicked() }
}
@Composable
override fun View(modifier: Modifier) {
val state = presenter.present()
val context = LocalContext.current
- ChangeServerView(
+ ChangeAccountProviderView(
state = state,
modifier = modifier,
- onChangeServerSuccess = this::onSuccess,
- onBackPressed = { navigateUp() },
+ onBackPressed = ::navigateUp,
onLearnMoreClicked = { openLearnMorePage(context) },
+ onDone = ::onDone,
+ onOtherProviderClicked = ::onOtherClicked,
)
}
}
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderPresenter.kt
new file mode 100644
index 0000000000..dfdb7dcf99
--- /dev/null
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderPresenter.kt
@@ -0,0 +1,47 @@
+/*
+ * 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.changeaccountprovider
+
+import androidx.compose.runtime.Composable
+import io.element.android.features.login.impl.accountprovider.AccountProvider
+import io.element.android.features.login.impl.changeserver.ChangeServerPresenter
+import io.element.android.libraries.architecture.Presenter
+import javax.inject.Inject
+
+class ChangeAccountProviderPresenter @Inject constructor(
+ private val changeServerPresenter: ChangeServerPresenter,
+) : Presenter {
+
+ @Composable
+ override fun present(): ChangeAccountProviderState {
+ val changeServerState = changeServerPresenter.present()
+ return ChangeAccountProviderState(
+ // Just matrix.org by default for now
+ accountProviders = listOf(
+ AccountProvider(
+ title = "matrix.org",
+ subtitle = null,
+ isPublic = true,
+ isMatrixOrg = true,
+ isValid = true,
+ supportSlidingSync = true,
+ )
+ ),
+ changeServerState = changeServerState,
+ )
+ }
+}
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderState.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderState.kt
new file mode 100644
index 0000000000..806ce5bc64
--- /dev/null
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderState.kt
@@ -0,0 +1,26 @@
+/*
+ * 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.changeaccountprovider
+
+import io.element.android.features.login.impl.accountprovider.AccountProvider
+import io.element.android.features.login.impl.changeserver.ChangeServerState
+
+// Do not use default value, so no member get forgotten in the presenters.
+data class ChangeAccountProviderState constructor(
+ val accountProviders: List,
+ val changeServerState: ChangeServerState,
+)
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderStateProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderStateProvider.kt
new file mode 100644
index 0000000000..403746f227
--- /dev/null
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderStateProvider.kt
@@ -0,0 +1,36 @@
+/*
+ * 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.changeaccountprovider
+
+import androidx.compose.ui.tooling.preview.PreviewParameterProvider
+import io.element.android.features.login.impl.accountprovider.anAccountProvider
+import io.element.android.features.login.impl.changeserver.aChangeServerState
+
+open class ChangeAccountProviderStateProvider : PreviewParameterProvider {
+ override val values: Sequence
+ get() = sequenceOf(
+ aChangeAccountProviderState(),
+ // Add other state here
+ )
+}
+
+fun aChangeAccountProviderState() = ChangeAccountProviderState(
+ accountProviders = listOf(
+ anAccountProvider()
+ ),
+ changeServerState = aChangeServerState(),
+)
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderView.kt
new file mode 100644
index 0000000000..0f444350c9
--- /dev/null
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderView.kt
@@ -0,0 +1,147 @@
+/*
+ * 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.
+ */
+
+@file:OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
+
+package io.element.android.features.login.impl.screens.changeaccountprovider
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.ExperimentalLayoutApi
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.consumeWindowInsets
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.imePadding
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Home
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.tooling.preview.PreviewParameter
+import androidx.compose.ui.unit.dp
+import io.element.android.features.login.impl.R
+import io.element.android.features.login.impl.accountprovider.AccountProvider
+import io.element.android.features.login.impl.accountprovider.AccountProviderView
+import io.element.android.features.login.impl.changeserver.ChangeServerEvents
+import io.element.android.features.login.impl.changeserver.ChangeServerView
+import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
+import io.element.android.libraries.designsystem.components.button.BackButton
+import io.element.android.libraries.designsystem.preview.ElementPreviewDark
+import io.element.android.libraries.designsystem.preview.ElementPreviewLight
+import io.element.android.libraries.designsystem.theme.components.Scaffold
+import io.element.android.libraries.designsystem.theme.components.TopAppBar
+
+/**
+ * https://www.figma.com/file/o9p34zmiuEpZRyvZXJZAYL/FTUE?type=design&node-id=604-60817
+ */
+@Composable
+fun ChangeAccountProviderView(
+ state: ChangeAccountProviderState,
+ onBackPressed: () -> Unit,
+ onLearnMoreClicked: () -> Unit,
+ onDone: () -> Unit,
+ onOtherProviderClicked: () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ Scaffold(
+ modifier = modifier,
+ topBar = {
+ TopAppBar(
+ title = {},
+ navigationIcon = { BackButton(onClick = onBackPressed) }
+ )
+ }
+ ) { padding ->
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .imePadding()
+ .padding(padding)
+ .consumeWindowInsets(padding)
+ ) {
+ Column(
+ modifier = Modifier
+ .verticalScroll(state = rememberScrollState())
+ ) {
+ IconTitleSubtitleMolecule(
+ modifier = Modifier.padding(top = 16.dp, bottom = 32.dp, start = 16.dp, end = 16.dp),
+ iconImageVector = Icons.Filled.Home,
+ iconTint = MaterialTheme.colorScheme.primary,
+ title = stringResource(id = R.string.screen_change_account_provider_title),
+ subTitle = stringResource(id = R.string.screen_change_account_provider_subtitle),
+ )
+
+ state.accountProviders.forEach { item ->
+ val alteredItem = if (item.isMatrixOrg) {
+ // Set the subtitle from the resource
+ item.copy(
+ subtitle = stringResource(id = R.string.screen_change_account_provider_matrix_org_subtitle),
+ )
+ } else {
+ item
+ }
+ AccountProviderView(
+ item = alteredItem,
+ onClick = {
+ state.changeServerState.eventSink.invoke(ChangeServerEvents.ChangeServer(alteredItem))
+ }
+ )
+ }
+ // Other
+ AccountProviderView(
+ item = AccountProvider(
+ title = stringResource(id = R.string.screen_change_account_provider_other),
+ ),
+ onClick = onOtherProviderClicked
+ )
+ Spacer(Modifier.height(32.dp))
+ }
+ ChangeServerView(
+ state = state.changeServerState,
+ onLearnMoreClicked = onLearnMoreClicked,
+ onDone = onDone,
+ )
+ }
+ }
+}
+
+@Preview
+@Composable
+fun ChangeAccountProviderViewLightPreview(@PreviewParameter(ChangeAccountProviderStateProvider::class) state: ChangeAccountProviderState) =
+ ElementPreviewLight { ContentToPreview(state) }
+
+@Preview
+@Composable
+fun ChangeAccountProviderViewDarkPreview(@PreviewParameter(ChangeAccountProviderStateProvider::class) state: ChangeAccountProviderState) =
+ ElementPreviewDark { ContentToPreview(state) }
+
+@Composable
+private fun ContentToPreview(state: ChangeAccountProviderState) {
+ ChangeAccountProviderView(
+ state = state,
+ onBackPressed = { },
+ onLearnMoreClicked = { },
+ onDone = { },
+ onOtherProviderClicked = { },
+ )
+}
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderEvents.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderEvents.kt
new file mode 100644
index 0000000000..1ba3cc3028
--- /dev/null
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderEvents.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.features.login.impl.screens.confirmaccountprovider
+
+sealed interface ConfirmAccountProviderEvents {
+ object Continue : ConfirmAccountProviderEvents
+ object ClearError : ConfirmAccountProviderEvents
+}
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderNode.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderNode.kt
new file mode 100644
index 0000000000..7cef986013
--- /dev/null
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderNode.kt
@@ -0,0 +1,84 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.features.login.impl.screens.confirmaccountprovider
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+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.util.openLearnMorePage
+import io.element.android.libraries.architecture.NodeInputs
+import io.element.android.libraries.architecture.inputs
+import io.element.android.libraries.di.AppScope
+import io.element.android.libraries.matrix.api.auth.OidcDetails
+
+@ContributesNode(AppScope::class)
+class ConfirmAccountProviderNode @AssistedInject constructor(
+ @Assisted buildContext: BuildContext,
+ @Assisted plugins: List,
+ presenterFactory: ConfirmAccountProviderPresenter.Factory,
+) : Node(buildContext, plugins = plugins) {
+
+ data class Inputs(
+ val isAccountCreation: Boolean,
+ ) : NodeInputs
+
+ private val inputs: Inputs = inputs()
+ private val presenter = presenterFactory.create(
+ ConfirmAccountProviderPresenter.Params(
+ isAccountCreation = inputs.isAccountCreation,
+ )
+ )
+
+ interface Callback : Plugin {
+ fun onLoginPasswordNeeded()
+ fun onOidcDetails(oidcDetails: OidcDetails)
+ fun onChangeAccountProvider()
+ }
+
+ private fun onOidcDetails(data: OidcDetails) {
+ plugins().forEach { it.onOidcDetails(data) }
+ }
+
+ private fun onLoginPasswordNeeded() {
+ plugins().forEach { it.onLoginPasswordNeeded() }
+ }
+
+ private fun onChangeAccountProvider() {
+ plugins().forEach { it.onChangeAccountProvider() }
+ }
+
+ @Composable
+ override fun View(modifier: Modifier) {
+ val state = presenter.present()
+ val context = LocalContext.current
+ ConfirmAccountProviderView(
+ state = state,
+ modifier = modifier,
+ onOidcDetails = ::onOidcDetails,
+ onLoginPasswordNeeded = ::onLoginPasswordNeeded,
+ onChange = ::onChangeAccountProvider,
+ onLearnMoreClicked = { openLearnMorePage(context) },
+ )
+ }
+}
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenter.kt
new file mode 100644
index 0000000000..2626e56365
--- /dev/null
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenter.kt
@@ -0,0 +1,100 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.features.login.impl.screens.confirmaccountprovider
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+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.accountprovider.AccountProviderDataSource
+import io.element.android.features.login.impl.error.ChangeServerError
+import io.element.android.libraries.architecture.Async
+import io.element.android.libraries.architecture.Presenter
+import io.element.android.libraries.architecture.execute
+import io.element.android.libraries.core.data.tryOrNull
+import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+import java.net.URL
+
+class ConfirmAccountProviderPresenter @AssistedInject constructor(
+ @Assisted private val params: Params,
+ private val accountProviderDataSource: AccountProviderDataSource,
+ private val authenticationService: MatrixAuthenticationService
+) : Presenter {
+
+ data class Params(
+ val isAccountCreation: Boolean,
+ )
+
+ @AssistedFactory
+ interface Factory {
+ fun create(params: Params): ConfirmAccountProviderPresenter
+ }
+
+ @Composable
+ override fun present(): ConfirmAccountProviderState {
+ val accountProvider by accountProviderDataSource.flow().collectAsState()
+ val localCoroutineScope = rememberCoroutineScope()
+
+ val loginFlowAction: MutableState> = remember {
+ mutableStateOf(Async.Uninitialized)
+ }
+
+ fun handleEvents(event: ConfirmAccountProviderEvents) {
+ when (event) {
+ ConfirmAccountProviderEvents.Continue -> {
+ localCoroutineScope.submit(accountProvider.title, loginFlowAction)
+ }
+ ConfirmAccountProviderEvents.ClearError -> loginFlowAction.value = Async.Uninitialized
+ }
+ }
+
+ return ConfirmAccountProviderState(
+ accountProvider = accountProvider,
+ isAccountCreation = params.isAccountCreation,
+ loginFlow = loginFlowAction.value,
+ eventSink = ::handleEvents
+ )
+ }
+
+ private fun CoroutineScope.submit(
+ homeserverUrl: String,
+ loginFlowAction: MutableState>,
+ ) = launch {
+ suspend {
+ val domain = tryOrNull { URL(homeserverUrl) }?.host ?: homeserverUrl
+ authenticationService.setHomeserver(domain).map {
+ val matrixHomeServerDetails = authenticationService.getHomeserverDetails().value!!
+ if (matrixHomeServerDetails.supportsOidcLogin) {
+ // Retrieve the details right now
+ LoginFlow.OidcFlow(authenticationService.getOidcUrl().getOrThrow())
+ } else if (matrixHomeServerDetails.supportsPasswordLogin) {
+ LoginFlow.PasswordLogin
+ } else {
+ throw IllegalStateException("Unsupported login flow")
+ }
+ }.getOrThrow()
+ }.execute(loginFlowAction, errorMapping = ChangeServerError::from)
+ }
+}
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderState.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderState.kt
new file mode 100644
index 0000000000..a870b88c58
--- /dev/null
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderState.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.features.login.impl.screens.confirmaccountprovider
+
+import io.element.android.features.login.impl.accountprovider.AccountProvider
+import io.element.android.libraries.architecture.Async
+import io.element.android.libraries.matrix.api.auth.OidcDetails
+
+// Do not use default value, so no member get forgotten in the presenters.
+data class ConfirmAccountProviderState(
+ val accountProvider: AccountProvider,
+ val isAccountCreation: Boolean,
+ val loginFlow: Async,
+ val eventSink: (ConfirmAccountProviderEvents) -> Unit
+) {
+ val submitEnabled: Boolean get() = accountProvider.title.isNotEmpty() && (loginFlow is Async.Uninitialized || loginFlow is Async.Loading)
+}
+
+sealed interface LoginFlow {
+ object PasswordLogin : LoginFlow
+ data class OidcFlow(val oidcDetails: OidcDetails) : LoginFlow
+}
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderStateProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderStateProvider.kt
new file mode 100644
index 0000000000..d5f98f5716
--- /dev/null
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderStateProvider.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.features.login.impl.screens.confirmaccountprovider
+
+import androidx.compose.ui.tooling.preview.PreviewParameterProvider
+import io.element.android.features.login.impl.accountprovider.anAccountProvider
+import io.element.android.libraries.architecture.Async
+
+open class ConfirmAccountProviderStateProvider : PreviewParameterProvider {
+ override val values: Sequence
+ get() = sequenceOf(
+ aConfirmAccountProviderState(),
+ // Add other state here
+ )
+}
+
+fun aConfirmAccountProviderState() = ConfirmAccountProviderState(
+ accountProvider = anAccountProvider(),
+ isAccountCreation = false,
+ loginFlow = Async.Uninitialized,
+ eventSink = {}
+)
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderView.kt
new file mode 100644
index 0000000000..c9053d67c9
--- /dev/null
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderView.kt
@@ -0,0 +1,164 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.features.login.impl.screens.confirmaccountprovider
+
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.AccountCircle
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.tooling.preview.PreviewParameter
+import androidx.compose.ui.unit.dp
+import io.element.android.features.login.impl.R
+import io.element.android.features.login.impl.dialogs.SlidingSyncNotSupportedDialog
+import io.element.android.features.login.impl.error.ChangeServerError
+import io.element.android.libraries.architecture.Async
+import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule
+import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
+import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
+import io.element.android.libraries.designsystem.components.button.ButtonWithProgress
+import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
+import io.element.android.libraries.designsystem.preview.ElementPreviewDark
+import io.element.android.libraries.designsystem.preview.ElementPreviewLight
+import io.element.android.libraries.designsystem.theme.components.Text
+import io.element.android.libraries.designsystem.theme.components.TextButton
+import io.element.android.libraries.matrix.api.auth.OidcDetails
+import io.element.android.libraries.testtags.TestTags
+import io.element.android.libraries.testtags.testTag
+
+@Composable
+fun ConfirmAccountProviderView(
+ state: ConfirmAccountProviderState,
+ onOidcDetails: (OidcDetails) -> Unit,
+ onLoginPasswordNeeded: () -> Unit,
+ onLearnMoreClicked: () -> Unit,
+ onChange: () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ val isLoading by remember(state.loginFlow) {
+ derivedStateOf {
+ state.loginFlow is Async.Loading
+ }
+ }
+ val eventSink = state.eventSink
+
+ HeaderFooterPage(
+ modifier = modifier,
+ header = {
+ IconTitleSubtitleMolecule(
+ modifier = Modifier.padding(top = 60.dp),
+ iconImageVector = Icons.Filled.AccountCircle,
+ title = stringResource(
+ id = if (state.isAccountCreation) {
+ R.string.screen_account_provider_signup_title
+ } else {
+ R.string.screen_account_provider_signin_title
+ },
+ state.accountProvider.title
+ ),
+ subTitle = stringResource(
+ id = if (state.isAccountCreation) {
+ R.string.screen_account_provider_signup_subtitle
+ } else {
+ // Use same value for now.
+ R.string.screen_account_provider_signup_subtitle
+ },
+ )
+ )
+ },
+ footer = {
+ ButtonColumnMolecule {
+ ButtonWithProgress(
+ text = stringResource(id = R.string.screen_account_provider_continue),
+ showProgress = isLoading,
+ onClick = { eventSink.invoke(ConfirmAccountProviderEvents.Continue) },
+ enabled = state.submitEnabled,
+ modifier = Modifier
+ .fillMaxWidth()
+ .testTag(TestTags.loginContinue)
+ )
+ TextButton(
+ onClick = onChange,
+ enabled = true,
+ modifier = Modifier
+ .fillMaxWidth()
+ .testTag(TestTags.loginChangeServer)
+ ) {
+ Text(text = stringResource(id = R.string.screen_account_provider_change))
+ }
+ }
+ }
+ ) {
+ when (state.loginFlow) {
+ is Async.Failure -> {
+ when (val error = state.loginFlow.error) {
+ is ChangeServerError.Error -> {
+ ErrorDialog(
+ content = error.message(),
+ onDismiss = {
+ eventSink.invoke(ConfirmAccountProviderEvents.ClearError)
+ }
+ )
+ }
+ is ChangeServerError.SlidingSyncAlert -> {
+ SlidingSyncNotSupportedDialog(onLearnMoreClicked = {
+ onLearnMoreClicked()
+ eventSink(ConfirmAccountProviderEvents.ClearError)
+ }, onDismiss = {
+ eventSink(ConfirmAccountProviderEvents.ClearError)
+ })
+ }
+ }
+ }
+ is Async.Loading -> Unit // The Continue button shows the loading state
+ is Async.Success -> {
+ when (val loginFlowState = state.loginFlow.state) {
+ is LoginFlow.OidcFlow -> onOidcDetails(loginFlowState.oidcDetails)
+ LoginFlow.PasswordLogin -> onLoginPasswordNeeded()
+ }
+ }
+ Async.Uninitialized -> Unit
+ }
+ }
+}
+
+@Preview
+@Composable
+fun ConfirmAccountProviderViewLightPreview(@PreviewParameter(ConfirmAccountProviderStateProvider::class) state: ConfirmAccountProviderState) =
+ ElementPreviewLight { ContentToPreview(state) }
+
+@Preview
+@Composable
+fun ConfirmAccountProviderViewDarkPreview(@PreviewParameter(ConfirmAccountProviderStateProvider::class) state: ConfirmAccountProviderState) =
+ ElementPreviewDark { ContentToPreview(state) }
+
+@Composable
+private fun ContentToPreview(state: ConfirmAccountProviderState) {
+ ConfirmAccountProviderView(
+ state = state,
+ onOidcDetails = {},
+ onLoginPasswordNeeded = {},
+ onLearnMoreClicked = {},
+ onChange = {},
+ )
+}
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordEvents.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordEvents.kt
new file mode 100644
index 0000000000..e6f23ca418
--- /dev/null
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordEvents.kt
@@ -0,0 +1,24 @@
+/*
+ * 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.loginpassword
+
+sealed interface LoginPasswordEvents {
+ data class SetLogin(val login: String) : LoginPasswordEvents
+ data class SetPassword(val password: String) : LoginPasswordEvents
+ object Submit : LoginPasswordEvents
+ object ClearError : LoginPasswordEvents
+}
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
new file mode 100644
index 0000000000..630b08570c
--- /dev/null
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordNode.kt
@@ -0,0 +1,45 @@
+/*
+ * 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.loginpassword
+
+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 dagger.assisted.Assisted
+import dagger.assisted.AssistedInject
+import io.element.android.anvilannotations.ContributesNode
+import io.element.android.libraries.di.AppScope
+
+@ContributesNode(AppScope::class)
+class LoginPasswordNode @AssistedInject constructor(
+ @Assisted buildContext: BuildContext,
+ @Assisted plugins: List,
+ private val presenter: LoginPasswordPresenter,
+) : Node(buildContext, plugins = plugins) {
+
+ @Composable
+ override fun View(modifier: Modifier) {
+ val state = presenter.present()
+ LoginPasswordView(
+ state = state,
+ modifier = modifier,
+ onBackPressed = ::navigateUp
+ )
+ }
+}
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
new file mode 100644
index 0000000000..1fc4a10bbb
--- /dev/null
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordPresenter.kt
@@ -0,0 +1,90 @@
+/*
+ * 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.loginpassword
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+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.accountprovider.AccountProviderDataSource
+import io.element.android.libraries.architecture.Async
+import io.element.android.libraries.architecture.Presenter
+import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
+import io.element.android.libraries.matrix.api.core.SessionId
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+class LoginPasswordPresenter @Inject constructor(
+ private val authenticationService: MatrixAuthenticationService,
+ private val accountProviderDataSource: AccountProviderDataSource,
+) : Presenter {
+
+ @Composable
+ override fun present(): LoginPasswordState {
+ val localCoroutineScope = rememberCoroutineScope()
+ val loginAction: MutableState> = remember {
+ mutableStateOf(Async.Uninitialized)
+ }
+
+ val formState = rememberSaveable {
+ mutableStateOf(LoginFormState.Default)
+ }
+ val accountProvider by accountProviderDataSource.flow().collectAsState()
+
+ fun handleEvents(event: LoginPasswordEvents) {
+ when (event) {
+ is LoginPasswordEvents.SetLogin -> updateFormState(formState) {
+ copy(login = event.login)
+ }
+ is LoginPasswordEvents.SetPassword -> updateFormState(formState) {
+ copy(password = event.password)
+ }
+ LoginPasswordEvents.Submit -> {
+ localCoroutineScope.submit(formState.value, loginAction)
+ }
+ LoginPasswordEvents.ClearError -> loginAction.value = Async.Uninitialized
+ }
+ }
+
+ return LoginPasswordState(
+ accountProvider = accountProvider,
+ formState = formState.value,
+ loginAction = loginAction.value,
+ eventSink = ::handleEvents
+ )
+ }
+
+ private fun CoroutineScope.submit(formState: LoginFormState, loggedInState: MutableState>) = launch {
+ 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)
+ }
+ }
+
+ private fun updateFormState(formState: MutableState, updateLambda: LoginFormState.() -> LoginFormState) {
+ formState.value = updateLambda(formState.value)
+ }
+}
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootState.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordState.kt
similarity index 50%
rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootState.kt
rename to features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordState.kt
index 45eafa744c..c8fa2f4ad3 100644
--- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootState.kt
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordState.kt
@@ -14,36 +14,23 @@
* limitations under the License.
*/
-package io.element.android.features.login.impl.root
+package io.element.android.features.login.impl.screens.loginpassword
import android.os.Parcelable
+import io.element.android.features.login.impl.accountprovider.AccountProvider
import io.element.android.libraries.architecture.Async
-import io.element.android.libraries.core.bool.orFalse
-import io.element.android.libraries.matrix.api.auth.MatrixHomeServerDetails
-import io.element.android.libraries.matrix.api.auth.OidcDetails
import io.element.android.libraries.matrix.api.core.SessionId
import kotlinx.parcelize.Parcelize
-data class LoginRootState(
- val homeserverUrl: String,
- val homeserverDetails: Async,
- val loggedInState: LoggedInState,
+data class LoginPasswordState(
+ val accountProvider: AccountProvider,
val formState: LoginFormState,
- val eventSink: (LoginRootEvents) -> Unit
+ val loginAction: Async,
+ val eventSink: (LoginPasswordEvents) -> Unit
) {
- val supportPasswordLogin = (homeserverDetails as? Async.Success)?.state?.supportsPasswordLogin.orFalse()
- val supportOidcLogin = (homeserverDetails as? Async.Success)?.state?.supportsOidcLogin.orFalse()
val submitEnabled: Boolean
- get() = loggedInState !is LoggedInState.ErrorLoggingIn &&
- ((formState.login.isNotEmpty() && formState.password.isNotEmpty()) || supportOidcLogin)
-}
-
-sealed interface LoggedInState {
- object NotLoggedIn : LoggedInState
- object LoggingIn : LoggedInState
- data class OidcStarted(val oidcDetail: OidcDetails) : LoggedInState
- data class ErrorLoggingIn(val failure: Throwable) : LoggedInState
- data class LoggedIn(val sessionId: SessionId) : LoggedInState
+ get() = loginAction !is Async.Failure &&
+ ((formState.login.isNotEmpty() && formState.password.isNotEmpty()))
}
@Parcelize
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordStateProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordStateProvider.kt
new file mode 100644
index 0000000000..b4f5a84691
--- /dev/null
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordStateProvider.kt
@@ -0,0 +1,39 @@
+/*
+ * 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.loginpassword
+
+import androidx.compose.ui.tooling.preview.PreviewParameterProvider
+import io.element.android.features.login.impl.accountprovider.anAccountProvider
+import io.element.android.libraries.architecture.Async
+
+open class LoginPasswordStateProvider : PreviewParameterProvider {
+ override val values: Sequence
+ get() = sequenceOf(
+ aLoginPasswordState(),
+ // Loading
+ aLoginPasswordState().copy(loginAction = Async.Loading()),
+ // Error
+ aLoginPasswordState().copy(loginAction = Async.Failure(Exception("An error occurred"))),
+ )
+}
+
+fun aLoginPasswordState() = LoginPasswordState(
+ accountProvider = anAccountProvider(),
+ formState = LoginFormState.Default,
+ loginAction = Async.Uninitialized,
+ eventSink = {}
+)
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordView.kt
similarity index 60%
rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootView.kt
rename to features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordView.kt
index 2444418725..9154a0792f 100644
--- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootView.kt
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordView.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2022 New Vector Ltd
+ * Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,15 +14,11 @@
* limitations under the License.
*/
-package io.element.android.features.login.impl.root
+package io.element.android.features.login.impl.screens.loginpassword
-import androidx.compose.foundation.background
-import androidx.compose.foundation.clickable
-import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
-import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxSize
@@ -30,31 +26,25 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
-import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.ChevronRight
+import androidx.compose.material.icons.filled.AccountCircle
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Visibility
import androidx.compose.material.icons.filled.VisibilityOff
import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
-import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.autofill.AutofillType
-import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.FocusDirection
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
@@ -62,7 +52,6 @@ import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.input.VisualTransformation
-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
@@ -70,8 +59,7 @@ import io.element.android.features.login.impl.R
import io.element.android.features.login.impl.error.loginError
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.designsystem.ElementTextStyles
-import io.element.android.libraries.designsystem.components.async.AsyncFailure
-import io.element.android.libraries.designsystem.components.async.AsyncLoading
+import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.components.button.ButtonWithProgress
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
@@ -86,23 +74,20 @@ import io.element.android.libraries.designsystem.theme.components.TextField
import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.designsystem.theme.components.autofill
import io.element.android.libraries.designsystem.theme.components.onTabOrEnterKeyFocusNext
-import io.element.android.libraries.matrix.api.auth.OidcDetails
import io.element.android.libraries.testtags.TestTags
import io.element.android.libraries.testtags.testTag
import io.element.android.libraries.ui.strings.R as StringR
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
@Composable
-fun LoginRootView(
- state: LoginRootState,
+fun LoginPasswordView(
+ state: LoginPasswordState,
modifier: Modifier = Modifier,
- onChangeServer: () -> Unit = {},
- onOidcDetails: (OidcDetails) -> Unit = {},
onBackPressed: () -> Unit,
) {
- val isLoading by remember(state.loggedInState) {
+ val isLoading by remember(state.loginAction) {
derivedStateOf {
- state.loggedInState == LoggedInState.LoggingIn
+ state.loginAction is Async.Loading
}
}
val focusManager = LocalFocusManager.current
@@ -111,10 +96,11 @@ fun LoginRootView(
// Clear focus to prevent keyboard issues with textfields
focusManager.clearFocus(force = true)
- state.eventSink(LoginRootEvents.Submit)
+ state.eventSink(LoginPasswordEvents.Submit)
}
Scaffold(
+ modifier = modifier,
topBar = {
TopAppBar(
title = {},
@@ -123,7 +109,7 @@ fun LoginRootView(
}
) { padding ->
Box(
- modifier = modifier
+ modifier = Modifier
.fillMaxSize()
.imePadding()
.padding(padding)
@@ -136,142 +122,48 @@ fun LoginRootView(
.verticalScroll(state = scrollState)
.padding(horizontal = 16.dp),
) {
- Spacer(Modifier.height(16.dp))
// Title
- Text(
- text = stringResource(id = R.string.screen_login_title),
+ IconTitleSubtitleMolecule(
+ modifier = Modifier.padding(top = 20.dp),
+ iconImageVector = Icons.Filled.AccountCircle,
+ title = stringResource(
+ id = R.string.screen_account_provider_signin_title,
+ state.accountProvider.title
+ ),
+ subTitle = stringResource(id = R.string.screen_login_form_header)
+ )
+ Spacer(Modifier.height(32.dp))
+ LoginForm(state = state,
+ isLoading = isLoading,
+ onSubmit = ::submit
+ )
+ Spacer(Modifier.height(28.dp))
+ // Submit
+ ButtonWithProgress(
+ text = stringResource(R.string.screen_login_submit),
+ showProgress = isLoading,
+ onClick = ::submit,
+ enabled = state.submitEnabled,
modifier = Modifier
- .fillMaxWidth(),
- style = ElementTextStyles.Bold.title1,
- color = MaterialTheme.colorScheme.primary,
+ .fillMaxWidth()
+ .testTag(TestTags.loginContinue)
)
- Spacer(Modifier.height(32.dp))
-
- ChangeServerSection(
- interactionEnabled = !isLoading,
- homeserver = state.homeserverUrl,
- onChangeServer = onChangeServer
- )
-
- Spacer(Modifier.height(32.dp))
-
- when (state.homeserverDetails) {
- Async.Uninitialized,
- is Async.Loading -> AsyncLoading()
- is Async.Failure -> AsyncFailure(
- throwable = state.homeserverDetails.error,
- onRetry = {
- state.eventSink.invoke(LoginRootEvents.RetryFetchServerInfo)
- }
- )
- is Async.Success -> ServerDetailForm(state, isLoading, ::submit)
- }
+ Spacer(modifier = Modifier.height(32.dp))
}
- when (val loggedInState = state.loggedInState) {
- is LoggedInState.OidcStarted -> onOidcDetails(loggedInState.oidcDetail)
- else -> Unit
+
+ if (state.loginAction is Async.Failure) {
+ LoginErrorDialog(error = state.loginAction.error, onDismiss = {
+ state.eventSink(LoginPasswordEvents.ClearError)
+ })
}
}
}
-
- if (state.loggedInState is LoggedInState.ErrorLoggingIn) {
- LoginErrorDialog(error = state.loggedInState.failure, onDismiss = {
- state.eventSink(LoginRootEvents.ClearError)
- })
- }
-}
-
-@Composable
-fun ServerDetailForm(
- state: LoginRootState,
- isLoading: Boolean,
- submit: () -> Unit,
- modifier: Modifier = Modifier,
-) {
- when {
- state.supportOidcLogin -> {
- // Oidc, in this case, just display a Spacer and the submit button
- Spacer(modifier.height(28.dp))
- }
- state.supportPasswordLogin -> {
- LoginForm(state = state, isLoading = isLoading, onSubmit = submit, modifier = modifier)
- }
- else -> {
- Text(modifier = modifier, text = "No supported login flow")
- }
- }
-
- Spacer(Modifier.height(28.dp))
-
- if (state.supportOidcLogin || state.supportPasswordLogin) {
- // Submit
- ButtonWithProgress(
- text = stringResource(R.string.screen_login_submit),
- showProgress = isLoading,
- onClick = submit,
- enabled = state.submitEnabled,
- modifier = Modifier
- .fillMaxWidth()
- .testTag(TestTags.loginContinue)
- )
- Spacer(modifier = Modifier.height(32.dp))
- }
-}
-
-@Composable
-internal fun ChangeServerSection(
- interactionEnabled: Boolean,
- homeserver: String,
- onChangeServer: () -> Unit,
- modifier: Modifier = Modifier
-) {
- Column(modifier) {
- Text(
- modifier = Modifier.padding(start = 16.dp, bottom = 8.dp),
- text = stringResource(id = R.string.screen_login_server_header),
- style = ElementTextStyles.Regular.formHeader,
- )
- Row(
- modifier = Modifier
- .fillMaxWidth()
- .clip(RoundedCornerShape(14.dp))
- .background(MaterialTheme.colorScheme.surfaceVariant)
- .testTag(TestTags.loginChangeServer)
- .clickable {
- if (interactionEnabled) {
- onChangeServer()
- }
- },
- horizontalArrangement = Arrangement.End,
- verticalAlignment = Alignment.CenterVertically,
- ) {
- Text(
- text = homeserver,
- style = ElementTextStyles.Bold.body,
- textAlign = TextAlign.Start,
- modifier = Modifier
- .weight(1f)
- .padding(horizontal = 16.dp, vertical = 16.dp)
- )
- IconButton(
- modifier = Modifier.size(24.dp),
- onClick = {
- if (interactionEnabled) {
- onChangeServer()
- }
- }
- ) {
- Icon(imageVector = Icons.Default.ChevronRight, contentDescription = null, tint = MaterialTheme.colorScheme.tertiary)
- }
- Spacer(Modifier.width(8.dp))
- }
- }
}
@OptIn(ExperimentalComposeUiApi::class)
@Composable
internal fun LoginForm(
- state: LoginRootState,
+ state: LoginPasswordState,
isLoading: Boolean,
onSubmit: () -> Unit,
modifier: Modifier = Modifier
@@ -299,14 +191,14 @@ internal fun LoginForm(
.testTag(TestTags.loginEmailUsername)
.autofill(autofillTypes = listOf(AutofillType.Username), onFill = {
loginFieldState = it
- eventSink(LoginRootEvents.SetLogin(it))
+ eventSink(LoginPasswordEvents.SetLogin(it))
}),
label = {
Text(text = stringResource(R.string.screen_login_username_hint))
},
onValueChange = {
loginFieldState = it
- eventSink(LoginRootEvents.SetLogin(it))
+ eventSink(LoginPasswordEvents.SetLogin(it))
},
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Email,
@@ -316,7 +208,6 @@ internal fun LoginForm(
focusManager.moveFocus(FocusDirection.Down)
}),
singleLine = true,
- maxLines = 1,
trailingIcon = if (loginFieldState.isNotEmpty()) {
{
IconButton(onClick = {
@@ -329,7 +220,7 @@ internal fun LoginForm(
)
var passwordVisible by remember { mutableStateOf(false) }
- if (state.loggedInState is LoggedInState.LoggingIn) {
+ if (state.loginAction is Async.Loading) {
// Ensure password is hidden when user submits the form
passwordVisible = false
}
@@ -343,11 +234,11 @@ internal fun LoginForm(
.testTag(TestTags.loginPassword)
.autofill(autofillTypes = listOf(AutofillType.Password), onFill = {
passwordFieldState = it
- eventSink(LoginRootEvents.SetPassword(it))
+ eventSink(LoginPasswordEvents.SetPassword(it))
}),
onValueChange = {
passwordFieldState = it
- eventSink(LoginRootEvents.SetPassword(it))
+ eventSink(LoginPasswordEvents.SetPassword(it))
},
label = {
Text(text = stringResource(R.string.screen_login_password_hint))
@@ -371,7 +262,6 @@ internal fun LoginForm(
onDone = { onSubmit() }
),
singleLine = true,
- maxLines = 1,
)
}
}
@@ -386,17 +276,17 @@ internal fun LoginErrorDialog(error: Throwable, onDismiss: () -> Unit) {
@Preview
@Composable
-internal fun LoginRootScreenLightPreview(@PreviewParameter(LoginRootStateProvider::class) state: LoginRootState) =
+internal fun LoginPasswordViewLightPreview(@PreviewParameter(LoginPasswordStateProvider::class) state: LoginPasswordState) =
ElementPreviewLight { ContentToPreview(state) }
@Preview
@Composable
-internal fun LoginRootScreenDarkPreview(@PreviewParameter(LoginRootStateProvider::class) state: LoginRootState) =
+internal fun LoginPasswordViewDarkPreview(@PreviewParameter(LoginPasswordStateProvider::class) state: LoginPasswordState) =
ElementPreviewDark { ContentToPreview(state) }
@Composable
-private fun ContentToPreview(state: LoginRootState) {
- LoginRootView(
+private fun ContentToPreview(state: LoginPasswordState) {
+ LoginPasswordView(
state = state,
onBackPressed = {}
)
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderEvents.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderEvents.kt
new file mode 100644
index 0000000000..53ee45f644
--- /dev/null
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderEvents.kt
@@ -0,0 +1,25 @@
+/*
+ * 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.searchaccountprovider
+
+sealed interface SearchAccountProviderEvents {
+ /**
+ * The user has typed something, expect to get a list of matching account provider results
+ * in the state.
+ */
+ data class UserInput(val input: String) : SearchAccountProviderEvents
+}
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootNode.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderNode.kt
similarity index 67%
rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootNode.kt
rename to features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderNode.kt
index 787f5d0b48..7178a105f6 100644
--- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootNode.kt
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderNode.kt
@@ -14,10 +14,11 @@
* limitations under the License.
*/
-package io.element.android.features.login.impl.root
+package io.element.android.features.login.impl.screens.searchaccountprovider
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
@@ -25,38 +26,34 @@ 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.util.openLearnMorePage
import io.element.android.libraries.di.AppScope
-import io.element.android.libraries.matrix.api.auth.OidcDetails
@ContributesNode(AppScope::class)
-class LoginRootNode @AssistedInject constructor(
+class SearchAccountProviderNode @AssistedInject constructor(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
- private val presenter: LoginRootPresenter,
+ private val presenter: SearchAccountProviderPresenter,
) : Node(buildContext, plugins = plugins) {
interface Callback : Plugin {
- fun onChangeHomeServer()
- fun onOidcDetails(oidcDetails: OidcDetails)
+ fun onDone()
}
- private fun onChangeHomeServer() {
- plugins().forEach { it.onChangeHomeServer() }
- }
-
- private fun onOidcDetails(oidcDetails: OidcDetails) {
- plugins().forEach { it.onOidcDetails(oidcDetails) }
+ private fun onDone() {
+ plugins().forEach { it.onDone() }
}
@Composable
override fun View(modifier: Modifier) {
val state = presenter.present()
- LoginRootView(
+ val context = LocalContext.current
+ SearchAccountProviderView(
state = state,
modifier = modifier,
- onChangeServer = ::onChangeHomeServer,
- onOidcDetails = ::onOidcDetails,
- onBackPressed = ::navigateUp
+ onBackPressed = ::navigateUp,
+ onLearnMoreClicked = { openLearnMorePage(context) },
+ onDone = ::onDone,
)
}
}
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderPresenter.kt
new file mode 100644
index 0000000000..1d8271e394
--- /dev/null
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderPresenter.kt
@@ -0,0 +1,85 @@
+/*
+ * 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.searchaccountprovider
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import io.element.android.features.login.impl.changeserver.ChangeServerPresenter
+import io.element.android.features.login.impl.resolver.HomeserverData
+import io.element.android.features.login.impl.resolver.HomeserverResolver
+import io.element.android.libraries.architecture.Async
+import io.element.android.libraries.architecture.Presenter
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+class SearchAccountProviderPresenter @Inject constructor(
+ private val homeserverResolver: HomeserverResolver,
+ private val changeServerPresenter: ChangeServerPresenter,
+) : Presenter {
+
+ @Composable
+ override fun present(): SearchAccountProviderState {
+ var userInput by rememberSaveable {
+ mutableStateOf("")
+ }
+ val changeServerState = changeServerPresenter.present()
+
+ val data: MutableState>> = remember {
+ mutableStateOf(Async.Uninitialized)
+ }
+
+ LaunchedEffect(userInput) {
+ onUserInput(userInput, data)
+ }
+
+ fun handleEvents(event: SearchAccountProviderEvents) {
+ when (event) {
+ is SearchAccountProviderEvents.UserInput -> {
+ userInput = event.input
+ }
+ }
+ }
+
+ return SearchAccountProviderState(
+ userInput = userInput,
+ userInputResult = data.value,
+ changeServerState = changeServerState,
+ eventSink = ::handleEvents
+ )
+ }
+
+ private fun CoroutineScope.onUserInput(userInput: String, data: MutableState>>) = launch {
+ data.value = Async.Uninitialized
+ // Debounce
+ delay(300)
+ data.value = Async.Loading()
+ homeserverResolver.resolve(userInput).collect {
+ data.value = Async.Success(it)
+ }
+ if (data.value !is Async.Success) {
+ data.value = Async.Uninitialized
+ }
+ }
+}
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderState.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderState.kt
new file mode 100644
index 0000000000..15859afde1
--- /dev/null
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderState.kt
@@ -0,0 +1,29 @@
+/*
+ * 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.searchaccountprovider
+
+import io.element.android.features.login.impl.changeserver.ChangeServerState
+import io.element.android.features.login.impl.resolver.HomeserverData
+import io.element.android.libraries.architecture.Async
+
+// Do not use default value, so no member get forgotten in the presenters.
+data class SearchAccountProviderState(
+ val userInput: String,
+ val userInputResult: Async>,
+ val changeServerState: ChangeServerState,
+ val eventSink: (SearchAccountProviderEvents) -> Unit
+)
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderStateProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderStateProvider.kt
new file mode 100644
index 0000000000..b6ffac8bd1
--- /dev/null
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderStateProvider.kt
@@ -0,0 +1,61 @@
+/*
+ * 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.searchaccountprovider
+
+import androidx.compose.ui.tooling.preview.PreviewParameterProvider
+import io.element.android.features.login.impl.changeserver.aChangeServerState
+import io.element.android.features.login.impl.resolver.HomeserverData
+import io.element.android.libraries.architecture.Async
+
+open class SearchAccountProviderStateProvider : PreviewParameterProvider {
+ override val values: Sequence
+ get() = sequenceOf(
+ aSearchAccountProviderState(),
+ aSearchAccountProviderState(userInputResult = Async.Success(aHomeserverDataList())),
+ // Add other state here
+ )
+}
+
+fun aSearchAccountProviderState(
+ userInput: String = "",
+ userInputResult: Async> = Async.Uninitialized,
+) = SearchAccountProviderState(
+ userInput = userInput,
+ userInputResult = userInputResult,
+ changeServerState = aChangeServerState(),
+ eventSink = {}
+)
+
+fun aHomeserverDataList(): List {
+ return listOf(
+ aHomeserverData(isWellknownValid = true, supportSlidingSync = true),
+ aHomeserverData(homeserverUrl = "https://no.sliding.sync", isWellknownValid = true, supportSlidingSync = false),
+ aHomeserverData(homeserverUrl = "https://invalid", isWellknownValid = false, supportSlidingSync = false),
+ )
+}
+
+fun aHomeserverData(
+ homeserverUrl: String = "https://matrix.org",
+ isWellknownValid: Boolean = true,
+ supportSlidingSync: Boolean = true,
+): HomeserverData {
+ return HomeserverData(
+ homeserverUrl = homeserverUrl,
+ isWellknownValid = isWellknownValid,
+ supportSlidingSync = supportSlidingSync,
+ )
+}
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderView.kt
new file mode 100644
index 0000000000..5c280cba0e
--- /dev/null
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderView.kt
@@ -0,0 +1,230 @@
+/*
+ * 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.
+ */
+
+@file:OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
+
+package io.element.android.features.login.impl.screens.searchaccountprovider
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.ExperimentalLayoutApi
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.consumeWindowInsets
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.imePadding
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.foundation.text.KeyboardActions
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Close
+import androidx.compose.material.icons.filled.Search
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.TopAppBar
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusDirection
+import androidx.compose.ui.platform.LocalFocusManager
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.input.ImeAction
+import androidx.compose.ui.text.input.KeyboardType
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.tooling.preview.PreviewParameter
+import androidx.compose.ui.unit.dp
+import io.element.android.features.login.impl.R
+import io.element.android.features.login.impl.accountprovider.AccountProvider
+import io.element.android.features.login.impl.accountprovider.AccountProviderView
+import io.element.android.features.login.impl.changeserver.ChangeServerEvents
+import io.element.android.features.login.impl.changeserver.ChangeServerView
+import io.element.android.features.login.impl.resolver.HomeserverData
+import io.element.android.libraries.architecture.Async
+import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
+import io.element.android.libraries.designsystem.components.button.BackButton
+import io.element.android.libraries.designsystem.components.form.textFieldState
+import io.element.android.libraries.designsystem.preview.ElementPreviewDark
+import io.element.android.libraries.designsystem.preview.ElementPreviewLight
+import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
+import io.element.android.libraries.designsystem.theme.components.Icon
+import io.element.android.libraries.designsystem.theme.components.IconButton
+import io.element.android.libraries.designsystem.theme.components.OutlinedTextField
+import io.element.android.libraries.designsystem.theme.components.Text
+import io.element.android.libraries.designsystem.theme.components.onTabOrEnterKeyFocusNext
+import io.element.android.libraries.testtags.TestTags
+import io.element.android.libraries.testtags.testTag
+import io.element.android.libraries.ui.strings.R as StringR
+
+/**
+ * https://www.figma.com/file/o9p34zmiuEpZRyvZXJZAYL/FTUE?type=design&node-id=611-61435
+ */
+@Composable
+fun SearchAccountProviderView(
+ state: SearchAccountProviderState,
+ onBackPressed: () -> Unit,
+ onLearnMoreClicked: () -> Unit,
+ onDone: () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ val eventSink = state.eventSink
+ Scaffold(
+ modifier = modifier,
+ topBar = {
+ TopAppBar(
+ title = {},
+ navigationIcon = { BackButton(onClick = onBackPressed) }
+ )
+ }
+ ) { padding ->
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .imePadding()
+ .padding(padding)
+ .consumeWindowInsets(padding)
+ ) {
+ LazyColumn(modifier = Modifier.fillMaxWidth(), state = rememberLazyListState()) {
+ item {
+ IconTitleSubtitleMolecule(
+ modifier = Modifier.padding(top = 16.dp, bottom = 40.dp, start = 16.dp, end = 16.dp),
+ iconImageVector = Icons.Filled.Search,
+ title = stringResource(id = R.string.screen_account_provider_form_title),
+ subTitle = stringResource(id = R.string.screen_account_provider_form_subtitle),
+ )
+ }
+ item {
+ // TextInput
+ var userInputState by textFieldState(stateValue = state.userInput)
+ val focusManager = LocalFocusManager.current
+ OutlinedTextField(
+ value = userInputState,
+ // readOnly = isLoading,
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(start = 16.dp, end = 16.dp, bottom = 16.dp)
+ .onTabOrEnterKeyFocusNext(focusManager)
+ .testTag(TestTags.changeServerServer),
+ onValueChange = {
+ userInputState = it
+ eventSink(SearchAccountProviderEvents.UserInput(it))
+ },
+ keyboardOptions = KeyboardOptions(
+ keyboardType = KeyboardType.Uri,
+ imeAction = ImeAction.Done,
+ ),
+ keyboardActions = KeyboardActions(onDone = {
+ focusManager.moveFocus(FocusDirection.Down)
+ }),
+ singleLine = true,
+ trailingIcon = if (userInputState.isNotEmpty()) {
+ {
+ IconButton(onClick = {
+ userInputState = ""
+ eventSink(SearchAccountProviderEvents.UserInput(""))
+ }) {
+ Icon(
+ imageVector = Icons.Filled.Close,
+ contentDescription = stringResource(StringR.string.action_clear)
+ )
+ }
+ }
+ } else null,
+ supportingText = {
+ Text(text = stringResource(id = R.string.screen_account_provider_form_notice), color = MaterialTheme.colorScheme.secondary)
+ }
+ )
+ }
+
+ when (state.userInputResult) {
+ is Async.Failure -> {
+ // Ignore errors (let the user type more chars)
+ }
+ is Async.Loading -> {
+ item {
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ ) {
+ CircularProgressIndicator(
+ modifier = Modifier.align(Alignment.Center)
+ )
+ }
+ }
+ }
+ is Async.Success -> {
+ items(state.userInputResult.state) { homeserverData ->
+ val item = homeserverData.toAccountProvider()
+ AccountProviderView(
+ item = item,
+ onClick = {
+ state.changeServerState.eventSink.invoke(ChangeServerEvents.ChangeServer(item))
+ }
+ )
+ }
+ }
+ Async.Uninitialized -> Unit
+ }
+ item {
+ Spacer(Modifier.height(32.dp))
+ }
+ }
+ ChangeServerView(
+ state = state.changeServerState,
+ onLearnMoreClicked = onLearnMoreClicked,
+ onDone = onDone,
+ )
+ }
+ }
+}
+
+@Composable
+private fun HomeserverData.toAccountProvider(): AccountProvider {
+ val isMatrixOrg = homeserverUrl == "https://matrix.org"
+ return AccountProvider(
+ title = homeserverUrl.removePrefix("http://").removePrefix("https://"),
+ subtitle = if (isMatrixOrg) stringResource(id = R.string.screen_change_account_provider_matrix_org_subtitle) else null,
+ isPublic = isMatrixOrg, // There is no need to know for other servers right now
+ isMatrixOrg = isMatrixOrg,
+ isValid = isWellknownValid,
+ supportSlidingSync = supportSlidingSync,
+ )
+}
+
+@Preview
+@Composable
+fun SearchAccountProviderViewLightPreview(@PreviewParameter(SearchAccountProviderStateProvider::class) state: SearchAccountProviderState) =
+ ElementPreviewLight { ContentToPreview(state) }
+
+@Preview
+@Composable
+fun SearchAccountProviderViewDarkPreview(@PreviewParameter(SearchAccountProviderStateProvider::class) state: SearchAccountProviderState) =
+ ElementPreviewDark { ContentToPreview(state) }
+
+@Composable
+private fun ContentToPreview(state: SearchAccountProviderState) {
+ SearchAccountProviderView(
+ state = state,
+ onBackPressed = {},
+ onLearnMoreClicked = {},
+ onDone = {},
+ )
+}
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/util/LoginConstants.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/util/LoginConstants.kt
index cb01f8095a..e8bcea990e 100644
--- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/util/LoginConstants.kt
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/util/LoginConstants.kt
@@ -16,8 +16,18 @@
package io.element.android.features.login.impl.util
+import io.element.android.features.login.impl.accountprovider.AccountProvider
+
object LoginConstants {
+ const val MATRIX_ORG_URL = "matrix.org"
const val DEFAULT_HOMESERVER_URL = "matrix.org" // TODO Oidc "synapse-oidc.lab.element.dev"
const val SLIDING_SYNC_READ_MORE_URL = "https://github.com/matrix-org/sliding-sync/blob/main/docs/Landing.md"
}
+
+val defaultAccountProvider = AccountProvider(
+ title = LoginConstants.DEFAULT_HOMESERVER_URL,
+ subtitle = null,
+ isPublic = LoginConstants.DEFAULT_HOMESERVER_URL == LoginConstants.MATRIX_ORG_URL,
+ isMatrixOrg = LoginConstants.DEFAULT_HOMESERVER_URL == LoginConstants.MATRIX_ORG_URL,
+)
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/util/Util.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/util/Util.kt
new file mode 100644
index 0000000000..261b02c1b8
--- /dev/null
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/util/Util.kt
@@ -0,0 +1,27 @@
+/*
+ * 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.util
+
+import android.content.Context
+import android.content.Intent
+import android.net.Uri
+import io.element.android.libraries.core.data.tryOrNull
+
+fun openLearnMorePage(context: Context) {
+ val intent = Intent(Intent.ACTION_VIEW, Uri.parse(LoginConstants.SLIDING_SYNC_READ_MORE_URL))
+ tryOrNull { context.startActivity(intent) }
+}
diff --git a/features/login/impl/src/main/res/drawable/ic_matrix.xml b/features/login/impl/src/main/res/drawable/ic_matrix.xml
new file mode 100644
index 0000000000..dbc788a031
--- /dev/null
+++ b/features/login/impl/src/main/res/drawable/ic_matrix.xml
@@ -0,0 +1,12 @@
+
+
+
+
diff --git a/features/login/impl/src/main/res/drawable/ic_public.xml b/features/login/impl/src/main/res/drawable/ic_public.xml
new file mode 100644
index 0000000000..fc1eacbc9f
--- /dev/null
+++ b/features/login/impl/src/main/res/drawable/ic_public.xml
@@ -0,0 +1,12 @@
+
+
+
+
diff --git a/features/login/impl/src/main/res/values/localazy.xml b/features/login/impl/src/main/res/values/localazy.xml
index cf59844e89..bb27ca51e1 100644
--- a/features/login/impl/src/main/res/values/localazy.xml
+++ b/features/login/impl/src/main/res/values/localazy.xml
@@ -1,5 +1,18 @@
+ "Change account provider"
+ "Continue"
+ "Homeserver address"
+ "Enter a search term or a domain address."
+ "Search for a company, community, or private server."
+ "Find an account provider"
+ "You’re about to sign in to %s"
+ "This is where you conversations will live — just like you would use an email provider to keep your emails."
+ "You’re about to create an account on %s"
+ "Matrix.org is an open network for secure, decentralized communication."
+ "Other"
+ "Use a different account provider, such as your own private server or a work account."
+ "Change account provider"
"We couldn\'t reach this homeserver. Please check that you have entered the homeserver URL correctly. If the URL is correct, contact your homeserver administrator for further help."
"This server currently doesn’t support sliding sync."
"Homeserver URL"
@@ -13,9 +26,15 @@
"Where your conversations live"
"Welcome back!"
"Sign in to %1$s"
+ "Change account provider"
+ "A private server for Element employees."
+ "Matrix is an open network for secure, decentralised communication."
+ "This is where your conversations will live — just like you would use an email provider to keep your emails."
+ "You’re about to sign in to %1$s"
+ "You’re about to create an account on %1$s"
"Continue"
"Select your server"
"Password"
"Continue"
"Username"
-
\ No newline at end of file
+
diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerPresenterTest.kt
index a30bd9449c..9aefafb382 100644
--- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerPresenterTest.kt
+++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerPresenterTest.kt
@@ -20,147 +20,72 @@ 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.util.LoginConstants
+import io.element.android.features.login.impl.accountprovider.AccountProvider
+import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.matrix.test.A_HOMESERVER
-import io.element.android.libraries.matrix.test.A_HOMESERVER_URL_2
-import io.element.android.libraries.matrix.test.A_THROWABLE
+import io.element.android.libraries.matrix.test.A_HOMESERVER_URL
import io.element.android.libraries.matrix.test.auth.FakeAuthenticationService
import kotlinx.coroutines.test.runTest
import org.junit.Test
class ChangeServerPresenterTest {
@Test
- fun `present - should start with default homeserver`() = runTest {
+ fun `present - initial state`() = runTest {
val presenter = ChangeServerPresenter(
FakeAuthenticationService(),
+ AccountProviderDataSource()
)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
- assertThat(initialState.homeserver).isEqualTo(LoginConstants.DEFAULT_HOMESERVER_URL)
- assertThat(initialState.submitEnabled).isTrue()
+ assertThat(initialState.changeServerAction).isEqualTo(Async.Uninitialized)
}
}
@Test
- fun `present - authentication service can provide a homeserver`() = runTest {
- val presenter = ChangeServerPresenter(
- FakeAuthenticationService().apply {
- givenHomeserver(A_HOMESERVER.copy(url = A_HOMESERVER_URL_2))
- },
- )
- moleculeFlow(RecompositionClock.Immediate) {
- presenter.present()
- }.test {
- val initialState = awaitItem()
- assertThat(initialState.homeserver).isEqualTo(A_HOMESERVER_URL_2)
- assertThat(initialState.submitEnabled).isTrue()
- }
- }
-
- @Test
- fun `present - disable if empty or not correct`() = runTest {
- val presenter = ChangeServerPresenter(
- FakeAuthenticationService(),
- )
- moleculeFlow(RecompositionClock.Immediate) {
- presenter.present()
- }.test {
- val initialState = awaitItem()
- initialState.eventSink.invoke(ChangeServerEvents.SetServer(""))
- val emptyState = awaitItem()
- assertThat(emptyState.homeserver).isEqualTo("")
- assertThat(emptyState.submitEnabled).isFalse()
- }
- }
-
- @Test
- fun `present - submit`() = runTest {
- val presenter = ChangeServerPresenter(
- FakeAuthenticationService(),
- )
- moleculeFlow(RecompositionClock.Immediate) {
- presenter.present()
- }.test {
- val initialState = awaitItem()
- initialState.eventSink.invoke(ChangeServerEvents.Submit)
- val loadingState = awaitItem()
- assertThat(loadingState.submitEnabled).isTrue()
- assertThat(loadingState.changeServerAction).isInstanceOf(Async.Loading::class.java)
- val successState = awaitItem()
- assertThat(successState.submitEnabled).isFalse()
- assertThat(successState.changeServerAction).isInstanceOf(Async.Success::class.java)
- }
- }
-
- @Test
- fun `present - submit parses URL`() = runTest {
- val presenter = ChangeServerPresenter(
- FakeAuthenticationService(),
- )
- moleculeFlow(RecompositionClock.Immediate) {
- presenter.present()
- }.test {
- val longUrl = "https://matrix.org/.well-known/"
- val initialState = awaitItem()
- initialState.eventSink.invoke(ChangeServerEvents.SetServer(longUrl))
- awaitItem()
- initialState.eventSink.invoke(ChangeServerEvents.Submit)
- val loadingState = awaitItem()
- assertThat(loadingState.submitEnabled).isTrue()
- assertThat(loadingState.changeServerAction).isInstanceOf(Async.Loading::class.java)
- awaitItem() // Skip changing the url to the parsed domain
- val successState = awaitItem()
- assertThat(successState.submitEnabled).isFalse()
- assertThat(successState.changeServerAction).isInstanceOf(Async.Success::class.java)
- assertThat(successState.homeserver).isEqualTo("matrix.org")
- }
- }
-
- @Test
- fun `present - submit fails`() = runTest {
- val authServer = FakeAuthenticationService()
- val presenter = ChangeServerPresenter(authServer)
- moleculeFlow(RecompositionClock.Immediate) {
- presenter.present()
- }.test {
- val initialState = awaitItem()
- authServer.givenChangeServerError(Throwable())
- initialState.eventSink.invoke(ChangeServerEvents.Submit)
- skipItems(1) // Loading
- val failureState = awaitItem()
- assertThat(failureState.submitEnabled).isFalse()
- assertThat(failureState.changeServerAction).isInstanceOf(Async.Failure::class.java)
- }
- }
-
- @Test
- fun `present - clear error`() = runTest {
+ fun `present - change server ok`() = runTest {
val authenticationService = FakeAuthenticationService()
val presenter = ChangeServerPresenter(
authenticationService,
+ AccountProviderDataSource()
)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
+ assertThat(initialState.changeServerAction).isEqualTo(Async.Uninitialized)
+ authenticationService.givenHomeserver(A_HOMESERVER)
+ initialState.eventSink.invoke(ChangeServerEvents.ChangeServer(AccountProvider(A_HOMESERVER_URL)))
+ val loadingState = awaitItem()
+ assertThat(loadingState.changeServerAction).isInstanceOf(Async.Loading::class.java)
+ val successState = awaitItem()
+ assertThat(successState.changeServerAction).isEqualTo(Async.Success(Unit))
+ }
+ }
- // Submit will return an error
- authenticationService.givenChangeServerError(A_THROWABLE)
- initialState.eventSink(ChangeServerEvents.Submit)
-
- skipItems(1) // Loading
-
- // Check an error was returned
- val submittedState = awaitItem()
- assertThat(submittedState.changeServerAction).isInstanceOf(Async.Failure::class.java)
-
- // Assert the error is then cleared
- submittedState.eventSink(ChangeServerEvents.ClearError)
- val clearedState = awaitItem()
- assertThat(clearedState.changeServerAction).isEqualTo(Async.Uninitialized)
+ @Test
+ fun `present - change server error`() = runTest {
+ val authenticationService = FakeAuthenticationService()
+ val presenter = ChangeServerPresenter(
+ authenticationService,
+ AccountProviderDataSource()
+ )
+ moleculeFlow(RecompositionClock.Immediate) {
+ presenter.present()
+ }.test {
+ val initialState = awaitItem()
+ assertThat(initialState.changeServerAction).isEqualTo(Async.Uninitialized)
+ initialState.eventSink.invoke(ChangeServerEvents.ChangeServer(AccountProvider(A_HOMESERVER_URL)))
+ val loadingState = awaitItem()
+ assertThat(loadingState.changeServerAction).isInstanceOf(Async.Loading::class.java)
+ val failureState = awaitItem()
+ assertThat(failureState.changeServerAction).isInstanceOf(Async.Failure::class.java)
+ // Clear error
+ failureState.eventSink.invoke(ChangeServerEvents.ClearError)
+ val finalState = awaitItem()
+ assertThat(finalState.changeServerAction).isEqualTo(Async.Uninitialized)
}
}
}
diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/resolver/network/FakeWellknownRequest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/resolver/network/FakeWellknownRequest.kt
new file mode 100644
index 0000000000..58c2bf82a3
--- /dev/null
+++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/resolver/network/FakeWellknownRequest.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.resolver.network
+
+class FakeWellknownRequest : WellknownRequest {
+ private var resultMap: Map = emptyMap()
+ fun givenResultMap(map: Map) {
+ resultMap = map
+ }
+
+ override suspend fun execute(baseUrl: String): WellKnown {
+ return resultMap[baseUrl] ?: error("No result provided for $baseUrl")
+ }
+}
diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/root/LoginRootPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/root/LoginRootPresenterTest.kt
deleted file mode 100644
index 0dee8d47c0..0000000000
--- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/root/LoginRootPresenterTest.kt
+++ /dev/null
@@ -1,308 +0,0 @@
-/*
- * Copyright (c) 2023 New Vector Ltd
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package io.element.android.features.login.impl.root
-
-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.api.oidc.OidcAction
-import io.element.android.features.login.impl.oidc.customtab.DefaultOidcActionFlow
-import io.element.android.features.login.impl.util.LoginConstants
-import io.element.android.libraries.architecture.Async
-import io.element.android.libraries.matrix.api.auth.MatrixHomeServerDetails
-import io.element.android.libraries.matrix.test.A_HOMESERVER
-import io.element.android.libraries.matrix.test.A_HOMESERVER_OIDC
-import io.element.android.libraries.matrix.test.A_PASSWORD
-import io.element.android.libraries.matrix.test.A_SESSION_ID
-import io.element.android.libraries.matrix.test.A_THROWABLE
-import io.element.android.libraries.matrix.test.A_USER_NAME
-import io.element.android.libraries.matrix.test.auth.A_OIDC_DATA
-import io.element.android.libraries.matrix.test.auth.FakeAuthenticationService
-import kotlinx.coroutines.test.runTest
-import org.junit.Test
-
-class LoginRootPresenterTest {
- @Test
- fun `present - initial state`() = runTest {
- val presenter = LoginRootPresenter(
- FakeAuthenticationService(),
- DefaultOidcActionFlow(),
- )
- moleculeFlow(RecompositionClock.Immediate) {
- presenter.present()
- }.test {
- val initialState = awaitItem()
- assertThat(initialState.homeserverUrl).isEqualTo(LoginConstants.DEFAULT_HOMESERVER_URL)
- assertThat(initialState.homeserverDetails).isEqualTo(Async.Uninitialized)
- assertThat(initialState.loggedInState).isEqualTo(LoggedInState.NotLoggedIn)
- assertThat(initialState.formState).isEqualTo(LoginFormState.Default)
- assertThat(initialState.submitEnabled).isFalse()
- cancelAndIgnoreRemainingEvents()
- }
- }
-
- @Test
- fun `present - initial state server load`() = runTest {
- val authenticationService = FakeAuthenticationService()
- val oidcActionFlow = DefaultOidcActionFlow()
- val presenter = LoginRootPresenter(
- authenticationService,
- oidcActionFlow,
- )
- moleculeFlow(RecompositionClock.Immediate) {
- presenter.present()
- }.test {
- val initialState = awaitItem()
- assertThat(initialState.homeserverUrl).isEqualTo(LoginConstants.DEFAULT_HOMESERVER_URL)
- assertThat(initialState.homeserverDetails).isEqualTo(Async.Uninitialized)
- assertThat(initialState.loggedInState).isEqualTo(LoggedInState.NotLoggedIn)
- assertThat(initialState.formState).isEqualTo(LoginFormState.Default)
- assertThat(initialState.submitEnabled).isFalse()
- val loadingState = awaitItem()
- assertThat(loadingState.homeserverDetails).isEqualTo(Async.Loading())
- authenticationService.givenHomeserver(A_HOMESERVER)
- skipItems(1)
- val loadedState = awaitItem()
- assertThat(loadedState.homeserverDetails).isEqualTo(Async.Success(A_HOMESERVER))
- }
- }
-
- @Test
- fun `present - initial state server load error and retry`() = runTest {
- val authenticationService = FakeAuthenticationService()
- val oidcActionFlow = DefaultOidcActionFlow()
- val presenter = LoginRootPresenter(
- authenticationService,
- oidcActionFlow,
- )
- moleculeFlow(RecompositionClock.Immediate) {
- presenter.present()
- }.test {
- val initialState = awaitItem()
- assertThat(initialState.homeserverUrl).isEqualTo(LoginConstants.DEFAULT_HOMESERVER_URL)
- assertThat(initialState.homeserverDetails).isEqualTo(Async.Uninitialized)
- assertThat(initialState.loggedInState).isEqualTo(LoggedInState.NotLoggedIn)
- assertThat(initialState.formState).isEqualTo(LoginFormState.Default)
- assertThat(initialState.submitEnabled).isFalse()
- val loadingState = awaitItem()
- assertThat(loadingState.homeserverDetails).isEqualTo(Async.Loading())
- val aThrowable = Throwable("Error")
- authenticationService.givenChangeServerError(aThrowable)
- val errorState = awaitItem()
- assertThat(errorState.homeserverDetails).isEqualTo(Async.Failure(aThrowable))
- // Retry
- errorState.eventSink.invoke(LoginRootEvents.RetryFetchServerInfo)
- val loadingState2 = awaitItem()
- assertThat(loadingState2.homeserverDetails).isEqualTo(Async.Loading())
- authenticationService.givenChangeServerError(null)
- authenticationService.givenHomeserver(A_HOMESERVER)
- skipItems(1)
- val loadedState = awaitItem()
- assertThat(loadedState.homeserverDetails).isEqualTo(Async.Success(A_HOMESERVER))
- }
- }
-
- @Test
- fun `present - enter login and password`() = runTest {
- val authenticationService = FakeAuthenticationService()
- val oidcActionFlow = DefaultOidcActionFlow()
- val presenter = LoginRootPresenter(
- authenticationService,
- oidcActionFlow,
- )
- authenticationService.givenHomeserver(A_HOMESERVER)
- moleculeFlow(RecompositionClock.Immediate) {
- presenter.present()
- }.test {
- val initialState = awaitItem()
- initialState.eventSink.invoke(LoginRootEvents.SetLogin(A_USER_NAME))
- val loginState = awaitItem()
- assertThat(loginState.formState).isEqualTo(LoginFormState(login = A_USER_NAME, password = ""))
- assertThat(loginState.submitEnabled).isFalse()
- initialState.eventSink.invoke(LoginRootEvents.SetPassword(A_PASSWORD))
- val loginAndPasswordState = awaitItem()
- assertThat(loginAndPasswordState.formState).isEqualTo(LoginFormState(login = A_USER_NAME, password = A_PASSWORD))
- assertThat(loginAndPasswordState.submitEnabled).isTrue()
- }
- }
-
- @Test
- fun `present - oidc login`() = runTest {
- val authenticationService = FakeAuthenticationService()
- val oidcActionFlow = DefaultOidcActionFlow()
- val presenter = LoginRootPresenter(
- authenticationService,
- oidcActionFlow,
- )
- authenticationService.givenHomeserver(A_HOMESERVER_OIDC)
- moleculeFlow(RecompositionClock.Immediate) {
- presenter.present()
- }.test {
- val initialState = awaitItem()
- assertThat(initialState.submitEnabled).isTrue()
- initialState.eventSink.invoke(LoginRootEvents.Submit)
- val oidcState = awaitItem()
- assertThat(oidcState.loggedInState).isEqualTo(LoggedInState.OidcStarted(A_OIDC_DATA))
- }
- }
-
- @Test
- fun `present - oidc login error`() = runTest {
- val authenticationService = FakeAuthenticationService()
- val oidcActionFlow = DefaultOidcActionFlow()
- val presenter = LoginRootPresenter(
- authenticationService,
- oidcActionFlow,
- )
- authenticationService.givenHomeserver(A_HOMESERVER_OIDC)
- authenticationService.givenOidcError(A_THROWABLE)
- moleculeFlow(RecompositionClock.Immediate) {
- presenter.present()
- }.test {
- val initialState = awaitItem()
- assertThat(initialState.submitEnabled).isTrue()
- initialState.eventSink.invoke(LoginRootEvents.Submit)
- val oidcState = awaitItem()
- assertThat(oidcState.loggedInState).isEqualTo(LoggedInState.ErrorLoggingIn(A_THROWABLE))
- }
- }
-
- @Test
- fun `present - oidc custom tab login`() = runTest {
- val authenticationService = FakeAuthenticationService()
- val oidcActionFlow = DefaultOidcActionFlow()
- val presenter = LoginRootPresenter(
- authenticationService,
- oidcActionFlow,
- )
- authenticationService.givenHomeserver(A_HOMESERVER_OIDC)
- moleculeFlow(RecompositionClock.Immediate) {
- presenter.present()
- }.test {
- val initialState = awaitItem()
- assertThat(initialState.submitEnabled).isTrue()
- initialState.eventSink.invoke(LoginRootEvents.Submit)
- val oidcState = awaitItem()
- assertThat(oidcState.loggedInState).isEqualTo(LoggedInState.OidcStarted(A_OIDC_DATA))
- // Oidc cancel, sdk error
- authenticationService.givenOidcCancelError(A_THROWABLE)
- oidcActionFlow.post(OidcAction.GoBack)
- val stateCancelSdkError = awaitItem()
- assertThat(stateCancelSdkError.loggedInState).isEqualTo(LoggedInState.ErrorLoggingIn(A_THROWABLE))
- // Oidc cancel, sdk OK
- authenticationService.givenOidcCancelError(null)
- oidcActionFlow.post(OidcAction.GoBack)
- val stateCancel = awaitItem()
- assertThat(stateCancel.loggedInState).isEqualTo(LoggedInState.NotLoggedIn)
- // Oidc success, sdk error
- authenticationService.givenLoginError(A_THROWABLE)
- oidcActionFlow.post(OidcAction.Success(A_OIDC_DATA.url))
- val stateSuccessSdkErrorLoading = awaitItem()
- assertThat(stateSuccessSdkErrorLoading.loggedInState).isEqualTo(LoggedInState.LoggingIn)
- val stateSuccessSdkError = awaitItem()
- assertThat(stateSuccessSdkError.loggedInState).isEqualTo(LoggedInState.ErrorLoggingIn(A_THROWABLE))
- // Oidc success
- authenticationService.givenLoginError(null)
- oidcActionFlow.post(OidcAction.Success(A_OIDC_DATA.url))
- val stateSuccess = awaitItem()
- assertThat(stateSuccess.loggedInState).isEqualTo(LoggedInState.LoggingIn)
- val stateSuccessLoggedIn = awaitItem()
- assertThat(stateSuccessLoggedIn.loggedInState).isEqualTo(LoggedInState.LoggedIn(A_SESSION_ID))
- }
- }
-
- @Test
- fun `present - submit`() = runTest {
- val authenticationService = FakeAuthenticationService()
- val oidcActionFlow = DefaultOidcActionFlow()
- val presenter = LoginRootPresenter(
- authenticationService,
- oidcActionFlow,
- )
- authenticationService.givenHomeserver(A_HOMESERVER)
- moleculeFlow(RecompositionClock.Immediate) {
- presenter.present()
- }.test {
- val initialState = awaitItem()
- initialState.eventSink.invoke(LoginRootEvents.SetLogin(A_USER_NAME))
- initialState.eventSink.invoke(LoginRootEvents.SetPassword(A_PASSWORD))
- skipItems(1)
- val loginAndPasswordState = awaitItem()
- loginAndPasswordState.eventSink.invoke(LoginRootEvents.Submit)
- val submitState = awaitItem()
- assertThat(submitState.loggedInState).isEqualTo(LoggedInState.LoggingIn)
- val loggedInState = awaitItem()
- assertThat(loggedInState.loggedInState).isEqualTo(LoggedInState.LoggedIn(A_SESSION_ID))
- }
- }
-
- @Test
- fun `present - submit with error`() = runTest {
- val authenticationService = FakeAuthenticationService()
- val oidcActionFlow = DefaultOidcActionFlow()
- val presenter = LoginRootPresenter(
- authenticationService,
- oidcActionFlow,
- )
- authenticationService.givenHomeserver(A_HOMESERVER)
- moleculeFlow(RecompositionClock.Immediate) {
- presenter.present()
- }.test {
- val initialState = awaitItem()
- initialState.eventSink.invoke(LoginRootEvents.SetLogin(A_USER_NAME))
- initialState.eventSink.invoke(LoginRootEvents.SetPassword(A_PASSWORD))
- skipItems(1)
- val loginAndPasswordState = awaitItem()
- authenticationService.givenLoginError(A_THROWABLE)
- loginAndPasswordState.eventSink.invoke(LoginRootEvents.Submit)
- val submitState = awaitItem()
- assertThat(submitState.loggedInState).isEqualTo(LoggedInState.LoggingIn)
- val loggedInState = awaitItem()
- assertThat(loggedInState.loggedInState).isEqualTo(LoggedInState.ErrorLoggingIn(A_THROWABLE))
- }
- }
-
- @Test
- fun `present - clear error`() = runTest {
- val authenticationService = FakeAuthenticationService()
- val oidcActionFlow = DefaultOidcActionFlow()
- val presenter = LoginRootPresenter(
- authenticationService,
- oidcActionFlow,
- )
- authenticationService.givenHomeserver(A_HOMESERVER)
- moleculeFlow(RecompositionClock.Immediate) {
- presenter.present()
- }.test {
- val initialState = awaitItem()
- // Submit will return an error
- authenticationService.givenLoginError(A_THROWABLE)
- initialState.eventSink(LoginRootEvents.Submit)
- awaitItem() // Skip LoggingIn state
-
- // Check an error was returned
- val submittedState = awaitItem()
- assertThat(submittedState.loggedInState).isEqualTo(LoggedInState.ErrorLoggingIn(A_THROWABLE))
-
- // Assert the error is then cleared
- submittedState.eventSink(LoginRootEvents.ClearError)
- val clearedState = awaitItem()
- assertThat(clearedState.loggedInState).isEqualTo(LoggedInState.NotLoggedIn)
- }
- }
-}
diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderPresenterTest.kt
new file mode 100644
index 0000000000..086428257a
--- /dev/null
+++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderPresenterTest.kt
@@ -0,0 +1,58 @@
+/*
+ * 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.changeaccountprovider
+
+import app.cash.molecule.RecompositionClock
+import app.cash.molecule.moleculeFlow
+import app.cash.turbine.test
+import com.google.common.truth.Truth.assertThat
+import io.element.android.features.login.impl.accountprovider.AccountProvider
+import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
+import io.element.android.features.login.impl.changeserver.ChangeServerPresenter
+import io.element.android.libraries.matrix.test.auth.FakeAuthenticationService
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+
+class ChangeAccountProviderPresenterTest {
+ @Test
+ fun `present - initial state`() = runTest {
+ val changeServerPresenter = ChangeServerPresenter(
+ FakeAuthenticationService(),
+ AccountProviderDataSource()
+ )
+ val presenter = ChangeAccountProviderPresenter(
+ changeServerPresenter
+ )
+ moleculeFlow(RecompositionClock.Immediate) {
+ presenter.present()
+ }.test {
+ val initialState = awaitItem()
+ assertThat(initialState.accountProviders).isEqualTo(
+ listOf(
+ AccountProvider(
+ title = "matrix.org",
+ subtitle = null,
+ isPublic = true,
+ isMatrixOrg = true,
+ isValid = true,
+ supportSlidingSync = true,
+ )
+ )
+ )
+ }
+ }
+}
diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenterTest.kt
new file mode 100644
index 0000000000..131d0d9298
--- /dev/null
+++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenterTest.kt
@@ -0,0 +1,150 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.features.login.impl.screens.confirmaccountprovider
+
+import app.cash.molecule.RecompositionClock
+import app.cash.molecule.moleculeFlow
+import app.cash.turbine.test
+import com.google.common.truth.Truth.assertThat
+import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
+import io.element.android.features.login.impl.util.defaultAccountProvider
+import io.element.android.libraries.architecture.Async
+import io.element.android.libraries.matrix.test.A_HOMESERVER
+import io.element.android.libraries.matrix.test.A_HOMESERVER_OIDC
+import io.element.android.libraries.matrix.test.A_THROWABLE
+import io.element.android.libraries.matrix.test.auth.FakeAuthenticationService
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+
+class ConfirmAccountProviderPresenterTest {
+ @Test
+ fun `present - initial test`() = runTest {
+ val presenter = ConfirmAccountProviderPresenter(
+ ConfirmAccountProviderPresenter.Params(isAccountCreation = false),
+ AccountProviderDataSource(),
+ FakeAuthenticationService(),
+ )
+ moleculeFlow(RecompositionClock.Immediate) {
+ presenter.present()
+ }.test {
+ val initialState = awaitItem()
+ assertThat(initialState.isAccountCreation).isFalse()
+ assertThat(initialState.submitEnabled).isTrue()
+ assertThat(initialState.accountProvider).isEqualTo(defaultAccountProvider)
+ assertThat(initialState.loginFlow).isEqualTo(Async.Uninitialized)
+ }
+ }
+
+ @Test
+ fun `present - continue password login`() = runTest {
+ val authServer = FakeAuthenticationService()
+ val presenter = ConfirmAccountProviderPresenter(
+ ConfirmAccountProviderPresenter.Params(isAccountCreation = false),
+ AccountProviderDataSource(),
+ authServer,
+ )
+ authServer.givenHomeserver(A_HOMESERVER)
+ moleculeFlow(RecompositionClock.Immediate) {
+ presenter.present()
+ }.test {
+ val initialState = awaitItem()
+ initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue)
+ val loadingState = awaitItem()
+ assertThat(loadingState.submitEnabled).isTrue()
+ assertThat(loadingState.loginFlow).isInstanceOf(Async.Loading::class.java)
+ val successState = awaitItem()
+ assertThat(successState.submitEnabled).isFalse()
+ assertThat(successState.loginFlow).isInstanceOf(Async.Success::class.java)
+ assertThat(successState.loginFlow.dataOrNull()).isEqualTo(LoginFlow.PasswordLogin)
+ }
+ }
+
+ @Test
+ fun `present - continue oidc`() = runTest {
+ val authServer = FakeAuthenticationService()
+ val presenter = ConfirmAccountProviderPresenter(
+ ConfirmAccountProviderPresenter.Params(isAccountCreation = false),
+ AccountProviderDataSource(),
+ authServer,
+ )
+ authServer.givenHomeserver(A_HOMESERVER_OIDC)
+ moleculeFlow(RecompositionClock.Immediate) {
+ presenter.present()
+ }.test {
+ val initialState = awaitItem()
+ initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue)
+ val loadingState = awaitItem()
+ assertThat(loadingState.submitEnabled).isTrue()
+ assertThat(loadingState.loginFlow).isInstanceOf(Async.Loading::class.java)
+ val successState = awaitItem()
+ assertThat(successState.submitEnabled).isFalse()
+ assertThat(successState.loginFlow).isInstanceOf(Async.Success::class.java)
+ assertThat(successState.loginFlow.dataOrNull()).isInstanceOf(LoginFlow.OidcFlow::class.java)
+ }
+ }
+
+ @Test
+ fun `present - submit fails`() = runTest {
+ val authServer = FakeAuthenticationService()
+ val presenter = ConfirmAccountProviderPresenter(
+ ConfirmAccountProviderPresenter.Params(isAccountCreation = false),
+ AccountProviderDataSource(),
+ authServer,
+ )
+ moleculeFlow(RecompositionClock.Immediate) {
+ presenter.present()
+ }.test {
+ val initialState = awaitItem()
+ authServer.givenChangeServerError(Throwable())
+ initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue)
+ skipItems(1) // Loading
+ val failureState = awaitItem()
+ assertThat(failureState.submitEnabled).isFalse()
+ assertThat(failureState.loginFlow).isInstanceOf(Async.Failure::class.java)
+ }
+ }
+
+ @Test
+ fun `present - clear error`() = runTest {
+ val authenticationService = FakeAuthenticationService()
+ val presenter = ConfirmAccountProviderPresenter(
+ ConfirmAccountProviderPresenter.Params(isAccountCreation = false),
+ AccountProviderDataSource(),
+ authenticationService,
+ )
+ moleculeFlow(RecompositionClock.Immediate) {
+ presenter.present()
+ }.test {
+ val initialState = awaitItem()
+
+ // Submit will return an error
+ authenticationService.givenChangeServerError(A_THROWABLE)
+ initialState.eventSink(ConfirmAccountProviderEvents.Continue)
+
+ skipItems(1) // Loading
+
+ // Check an error was returned
+ val submittedState = awaitItem()
+ assertThat(submittedState.loginFlow).isInstanceOf(Async.Failure::class.java)
+
+ // Assert the error is then cleared
+ submittedState.eventSink(ConfirmAccountProviderEvents.ClearError)
+ val clearedState = awaitItem()
+ assertThat(clearedState.loginFlow).isEqualTo(Async.Uninitialized)
+ }
+ }
+}
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
new file mode 100644
index 0000000000..c4c8a97155
--- /dev/null
+++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordPresenterTest.kt
@@ -0,0 +1,161 @@
+/*
+ * 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.loginpassword
+
+import app.cash.molecule.RecompositionClock
+import app.cash.molecule.moleculeFlow
+import app.cash.turbine.test
+import com.google.common.truth.Truth.assertThat
+import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
+import io.element.android.features.login.impl.util.defaultAccountProvider
+import io.element.android.libraries.architecture.Async
+import io.element.android.libraries.matrix.api.core.SessionId
+import io.element.android.libraries.matrix.test.A_HOMESERVER
+import io.element.android.libraries.matrix.test.A_PASSWORD
+import io.element.android.libraries.matrix.test.A_SESSION_ID
+import io.element.android.libraries.matrix.test.A_THROWABLE
+import io.element.android.libraries.matrix.test.A_USER_NAME
+import io.element.android.libraries.matrix.test.auth.FakeAuthenticationService
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+
+class LoginPasswordPresenterTest {
+ @Test
+ fun `present - initial state`() = runTest {
+ val authenticationService = FakeAuthenticationService()
+ val accountProviderDataSource = AccountProviderDataSource()
+ val presenter = LoginPasswordPresenter(
+ authenticationService,
+ accountProviderDataSource,
+ )
+ moleculeFlow(RecompositionClock.Immediate) {
+ presenter.present()
+ }.test {
+ val initialState = awaitItem()
+ assertThat(initialState.accountProvider).isEqualTo(defaultAccountProvider)
+ assertThat(initialState.formState).isEqualTo(LoginFormState.Default)
+ assertThat(initialState.loginAction).isEqualTo(Async.Uninitialized)
+ assertThat(initialState.submitEnabled).isFalse()
+ }
+ }
+
+ @Test
+ fun `present - enter login and password`() = runTest {
+ val authenticationService = FakeAuthenticationService()
+ val accountProviderDataSource = AccountProviderDataSource()
+ val presenter = LoginPasswordPresenter(
+ authenticationService,
+ accountProviderDataSource,
+ )
+ authenticationService.givenHomeserver(A_HOMESERVER)
+ moleculeFlow(RecompositionClock.Immediate) {
+ presenter.present()
+ }.test {
+ val initialState = awaitItem()
+ initialState.eventSink.invoke(LoginPasswordEvents.SetLogin(A_USER_NAME))
+ val loginState = awaitItem()
+ assertThat(loginState.formState).isEqualTo(LoginFormState(login = A_USER_NAME, password = ""))
+ assertThat(loginState.submitEnabled).isFalse()
+ initialState.eventSink.invoke(LoginPasswordEvents.SetPassword(A_PASSWORD))
+ val loginAndPasswordState = awaitItem()
+ assertThat(loginAndPasswordState.formState).isEqualTo(LoginFormState(login = A_USER_NAME, password = A_PASSWORD))
+ assertThat(loginAndPasswordState.submitEnabled).isTrue()
+ }
+ }
+
+ @Test
+ fun `present - submit`() = runTest {
+ val authenticationService = FakeAuthenticationService()
+ val accountProviderDataSource = AccountProviderDataSource()
+ val presenter = LoginPasswordPresenter(
+ authenticationService,
+ accountProviderDataSource,
+ )
+ authenticationService.givenHomeserver(A_HOMESERVER)
+ moleculeFlow(RecompositionClock.Immediate) {
+ presenter.present()
+ }.test {
+ val initialState = awaitItem()
+ initialState.eventSink.invoke(LoginPasswordEvents.SetLogin(A_USER_NAME))
+ initialState.eventSink.invoke(LoginPasswordEvents.SetPassword(A_PASSWORD))
+ skipItems(1)
+ val loginAndPasswordState = awaitItem()
+ loginAndPasswordState.eventSink.invoke(LoginPasswordEvents.Submit)
+ val submitState = awaitItem()
+ assertThat(submitState.loginAction).isInstanceOf(Async.Loading::class.java)
+ val loggedInState = awaitItem()
+ assertThat(loggedInState.loginAction).isEqualTo(Async.Success(A_SESSION_ID))
+ }
+ }
+
+ @Test
+ fun `present - submit with error`() = runTest {
+ val authenticationService = FakeAuthenticationService()
+ val accountProviderDataSource = AccountProviderDataSource()
+ val presenter = LoginPasswordPresenter(
+ authenticationService,
+ accountProviderDataSource,
+ )
+ authenticationService.givenHomeserver(A_HOMESERVER)
+ moleculeFlow(RecompositionClock.Immediate) {
+ presenter.present()
+ }.test {
+ val initialState = awaitItem()
+ initialState.eventSink.invoke(LoginPasswordEvents.SetLogin(A_USER_NAME))
+ initialState.eventSink.invoke(LoginPasswordEvents.SetPassword(A_PASSWORD))
+ skipItems(1)
+ val loginAndPasswordState = awaitItem()
+ authenticationService.givenLoginError(A_THROWABLE)
+ loginAndPasswordState.eventSink.invoke(LoginPasswordEvents.Submit)
+ val submitState = awaitItem()
+ assertThat(submitState.loginAction).isInstanceOf(Async.Loading::class.java)
+ val loggedInState = awaitItem()
+ assertThat(loggedInState.loginAction).isEqualTo(Async.Failure(A_THROWABLE))
+ }
+ }
+
+ @Test
+ fun `present - clear error`() = runTest {
+ val authenticationService = FakeAuthenticationService()
+ val accountProviderDataSource = AccountProviderDataSource()
+ val presenter = LoginPasswordPresenter(
+ authenticationService,
+ accountProviderDataSource,
+ )
+ authenticationService.givenHomeserver(A_HOMESERVER)
+ moleculeFlow(RecompositionClock.Immediate) {
+ presenter.present()
+ }.test {
+ val initialState = awaitItem()
+ initialState.eventSink.invoke(LoginPasswordEvents.SetLogin(A_USER_NAME))
+ initialState.eventSink.invoke(LoginPasswordEvents.SetPassword(A_PASSWORD))
+ skipItems(1)
+ val loginAndPasswordState = awaitItem()
+ authenticationService.givenLoginError(A_THROWABLE)
+ loginAndPasswordState.eventSink.invoke(LoginPasswordEvents.Submit)
+ val submitState = awaitItem()
+ assertThat(submitState.loginAction).isInstanceOf(Async.Loading::class.java)
+ val loggedInState = awaitItem()
+ // Check an error was returned
+ assertThat(loggedInState.loginAction).isEqualTo(Async.Failure(A_THROWABLE))
+ // Assert the error is then cleared
+ loggedInState.eventSink(LoginPasswordEvents.ClearError)
+ val clearedState = awaitItem()
+ assertThat(clearedState.loginAction).isEqualTo(Async.Uninitialized)
+ }
+ }
+}
diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderPresenterTest.kt
new file mode 100644
index 0000000000..9163f247f5
--- /dev/null
+++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderPresenterTest.kt
@@ -0,0 +1,195 @@
+/*
+ * 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.searchaccountprovider
+
+import app.cash.molecule.RecompositionClock
+import app.cash.molecule.moleculeFlow
+import app.cash.turbine.test
+import com.google.common.truth.Truth.assertThat
+import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
+import io.element.android.features.login.impl.changeserver.ChangeServerPresenter
+import io.element.android.features.login.impl.resolver.HomeserverResolver
+import io.element.android.features.login.impl.resolver.network.FakeWellknownRequest
+import io.element.android.features.login.impl.resolver.network.WellKnown
+import io.element.android.features.login.impl.resolver.network.WellKnownBaseConfig
+import io.element.android.features.login.impl.resolver.network.WellKnownSlidingSyncConfig
+import io.element.android.libraries.architecture.Async
+import io.element.android.libraries.matrix.test.A_HOMESERVER_URL
+import io.element.android.libraries.matrix.test.auth.FakeAuthenticationService
+import io.element.android.tests.testutils.testCoroutineDispatchers
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+
+class SearchAccountProviderPresenterTest {
+ @Test
+ fun `present - initial state`() = runTest {
+ val fakeWellknownRequest = FakeWellknownRequest()
+ val changeServerPresenter = ChangeServerPresenter(
+ FakeAuthenticationService(),
+ AccountProviderDataSource()
+ )
+ val presenter = SearchAccountProviderPresenter(
+ HomeserverResolver(testCoroutineDispatchers(), fakeWellknownRequest),
+ changeServerPresenter
+ )
+ moleculeFlow(RecompositionClock.Immediate) {
+ presenter.present()
+ }.test {
+ val initialState = awaitItem()
+ assertThat(initialState.userInput).isEmpty()
+ assertThat(initialState.userInputResult).isEqualTo(Async.Uninitialized)
+ }
+ }
+
+ @Test
+ fun `present - enter text no result`() = runTest {
+ val fakeWellknownRequest = FakeWellknownRequest()
+ val changeServerPresenter = ChangeServerPresenter(
+ FakeAuthenticationService(),
+ AccountProviderDataSource()
+ )
+ val presenter = SearchAccountProviderPresenter(
+ HomeserverResolver(testCoroutineDispatchers(), fakeWellknownRequest),
+ changeServerPresenter
+ )
+ moleculeFlow(RecompositionClock.Immediate) {
+ presenter.present()
+ }.test {
+ val initialState = awaitItem()
+ initialState.eventSink.invoke(SearchAccountProviderEvents.UserInput("test"))
+ val withInputState = awaitItem()
+ assertThat(withInputState.userInput).isEqualTo("test")
+ assertThat(initialState.userInputResult).isEqualTo(Async.Uninitialized)
+ assertThat(awaitItem().userInputResult).isInstanceOf(Async.Loading::class.java)
+ assertThat(awaitItem().userInputResult).isEqualTo(Async.Uninitialized)
+ }
+ }
+
+ @Test
+ fun `present - enter valid url no wellknown`() = runTest {
+ val fakeWellknownRequest = FakeWellknownRequest()
+ val changeServerPresenter = ChangeServerPresenter(
+ FakeAuthenticationService(),
+ AccountProviderDataSource()
+ )
+ val presenter = SearchAccountProviderPresenter(
+ HomeserverResolver(testCoroutineDispatchers(), fakeWellknownRequest),
+ changeServerPresenter
+ )
+ moleculeFlow(RecompositionClock.Immediate) {
+ presenter.present()
+ }.test {
+ val initialState = awaitItem()
+ initialState.eventSink.invoke(SearchAccountProviderEvents.UserInput("https://test.org"))
+ val withInputState = awaitItem()
+ assertThat(withInputState.userInput).isEqualTo("https://test.org")
+ assertThat(initialState.userInputResult).isEqualTo(Async.Uninitialized)
+ assertThat(awaitItem().userInputResult).isInstanceOf(Async.Loading::class.java)
+ assertThat(awaitItem().userInputResult).isEqualTo(
+ Async.Success(
+ listOf(
+ aHomeserverData(homeserverUrl = "https://test.org", isWellknownValid = false, supportSlidingSync = false)
+ )
+ )
+ )
+ }
+ }
+
+ @Test
+ fun `present - enter text one result no sliding sync`() = runTest {
+ val fakeWellknownRequest = FakeWellknownRequest()
+ fakeWellknownRequest.givenResultMap(
+ mapOf(
+ "https://test.org" to aWellKnown().copy(slidingSyncProxy = null),
+ )
+ )
+ val changeServerPresenter = ChangeServerPresenter(
+ FakeAuthenticationService(),
+ AccountProviderDataSource()
+ )
+ val presenter = SearchAccountProviderPresenter(
+ HomeserverResolver(testCoroutineDispatchers(), fakeWellknownRequest),
+ changeServerPresenter
+ )
+ moleculeFlow(RecompositionClock.Immediate) {
+ presenter.present()
+ }.test {
+ val initialState = awaitItem()
+ initialState.eventSink.invoke(SearchAccountProviderEvents.UserInput("test"))
+ val withInputState = awaitItem()
+ assertThat(withInputState.userInput).isEqualTo("test")
+ assertThat(initialState.userInputResult).isEqualTo(Async.Uninitialized)
+ assertThat(awaitItem().userInputResult).isInstanceOf(Async.Loading::class.java)
+ assertThat(awaitItem().userInputResult).isEqualTo(
+ Async.Success(
+ listOf(
+ aHomeserverData(homeserverUrl = "https://test.org", isWellknownValid = true, supportSlidingSync = false)
+ )
+ )
+ )
+ }
+ }
+
+ @Test
+ fun `present - enter text one result with sliding sync`() = runTest {
+ val fakeWellknownRequest = FakeWellknownRequest()
+ fakeWellknownRequest.givenResultMap(
+ mapOf(
+ "https://test.io" to aWellKnown(),
+ )
+ )
+ val changeServerPresenter = ChangeServerPresenter(
+ FakeAuthenticationService(),
+ AccountProviderDataSource()
+ )
+ val presenter = SearchAccountProviderPresenter(
+ HomeserverResolver(testCoroutineDispatchers(), fakeWellknownRequest),
+ changeServerPresenter
+ )
+ moleculeFlow(RecompositionClock.Immediate) {
+ presenter.present()
+ }.test {
+ val initialState = awaitItem()
+ initialState.eventSink.invoke(SearchAccountProviderEvents.UserInput("test"))
+ val withInputState = awaitItem()
+ assertThat(withInputState.userInput).isEqualTo("test")
+ assertThat(initialState.userInputResult).isEqualTo(Async.Uninitialized)
+ assertThat(awaitItem().userInputResult).isInstanceOf(Async.Loading::class.java)
+ assertThat(awaitItem().userInputResult).isEqualTo(
+ Async.Success(
+ listOf(
+ aHomeserverData(homeserverUrl = "https://test.io")
+ )
+ )
+ )
+ }
+ }
+
+ private fun aWellKnown(): WellKnown {
+ return WellKnown(
+ homeServer = WellKnownBaseConfig(
+ baseURL = A_HOMESERVER_URL
+ ),
+ identityServer = WellKnownBaseConfig(
+ baseURL = A_HOMESERVER_URL
+ ),
+ slidingSyncProxy = WellKnownSlidingSyncConfig(
+ url = A_HOMESERVER_URL
+ )
+ )
+ }
+}
diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/fixtures/timelineItemsFactory.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/fixtures/timelineItemsFactory.kt
index a5b1780589..beb37f8e51 100644
--- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/fixtures/timelineItemsFactory.kt
+++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/fixtures/timelineItemsFactory.kt
@@ -37,8 +37,9 @@ import io.element.android.libraries.dateformatter.test.FakeDaySeparatorFormatter
import io.element.android.libraries.eventformatter.api.TimelineEventFormatter
import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem
import io.element.android.tests.testutils.testCoroutineDispatchers
+import kotlinx.coroutines.test.TestScope
-internal fun aTimelineItemsFactory(): TimelineItemsFactory {
+internal fun TestScope.aTimelineItemsFactory(): TimelineItemsFactory {
val timelineEventFormatter = aTimelineEventFormatter()
return TimelineItemsFactory(
dispatchers = testCoroutineDispatchers(),
diff --git a/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/OnBoardingNode.kt b/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/OnBoardingNode.kt
index a081c0b7ab..d86623cae2 100644
--- a/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/OnBoardingNode.kt
+++ b/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/OnBoardingNode.kt
@@ -53,6 +53,7 @@ class OnBoardingNode @AssistedInject constructor(
state = state,
modifier = modifier,
onSignIn = ::onSignIn,
+ onCreateAccount = ::onSignUp,
)
}
}
diff --git a/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/OnBoardingView.kt b/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/OnBoardingView.kt
index 6f071fd090..643ab2bfd8 100644
--- a/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/OnBoardingView.kt
+++ b/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/OnBoardingView.kt
@@ -120,9 +120,7 @@ private fun OnBoardingButtons(
ButtonColumnMolecule(modifier = modifier) {
if (state.canLoginWithQrCode) {
Button(
- onClick = {
- onSignInWithQrCode()
- },
+ onClick = onSignInWithQrCode,
enabled = true,
modifier = Modifier
.fillMaxWidth()
@@ -136,9 +134,7 @@ private fun OnBoardingButtons(
}
}
Button(
- onClick = {
- onSignIn()
- },
+ onClick = onSignIn,
enabled = true,
modifier = Modifier
.fillMaxWidth()
@@ -148,9 +144,7 @@ private fun OnBoardingButtons(
}
if (state.canCreateAccount) {
OutlinedButton(
- onClick = {
- onCreateAccount()
- },
+ onClick = onCreateAccount,
enabled = true,
modifier = Modifier
.fillMaxWidth()
diff --git a/features/onboarding/impl/src/main/res/values/localazy.xml b/features/onboarding/impl/src/main/res/values/localazy.xml
index 54d86ba247..ef91de7541 100644
--- a/features/onboarding/impl/src/main/res/values/localazy.xml
+++ b/features/onboarding/impl/src/main/res/values/localazy.xml
@@ -4,6 +4,6 @@
"Sign in with QR code"
"Create account"
"Communicate and collaborate securely"
- "Welcome to the %1$s Beta. Supercharged, for speed and simplicity."
+ "Welcome to %1$s. Supercharged, for speed and simplicity."
"Be in your Element"
\ No newline at end of file
diff --git a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportView.kt b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportView.kt
index 05f3232aac..0e0ebaaab2 100644
--- a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportView.kt
+++ b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportView.kt
@@ -81,12 +81,9 @@ fun BugReportView(
.systemBarsPadding()
.imePadding()
) {
- val scrollState = rememberScrollState()
Column(
modifier = Modifier
- .verticalScroll(
- state = scrollState,
- )
+ .verticalScroll(state = rememberScrollState())
.padding(horizontal = 16.dp),
) {
val isError = state.sending is Async.Failure
diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersView.kt
index ced5e79bbe..867d2f433d 100644
--- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersView.kt
+++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersView.kt
@@ -147,7 +147,7 @@ private fun RoomInviteMembersSearchBar(
selectedUsers: ImmutableList,
active: Boolean,
modifier: Modifier = Modifier,
- placeHolderTitle: String = stringResource(io.element.android.libraries.ui.strings.R.string.common_search_for_someone),
+ placeHolderTitle: String = stringResource(StringR.string.common_search_for_someone),
onActiveChanged: (Boolean) -> Unit = {},
onTextChanged: (String) -> Unit = {},
onUserToggled: (MatrixUser) -> Unit = {},
diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/RoomDetailsPresenterTests.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/RoomDetailsPresenterTests.kt
index cf1d9a49ac..43f1667730 100644
--- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/RoomDetailsPresenterTests.kt
+++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/RoomDetailsPresenterTests.kt
@@ -252,7 +252,7 @@ class RoomDetailsPresenterTests {
fun aMatrixClient(
sessionId: SessionId = A_SESSION_ID,
-) = FakeMatrixClient()
+) = FakeMatrixClient(sessionId = sessionId)
fun aMatrixRoom(
roomId: RoomId = A_ROOM_ID,
diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersPresenterTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersPresenterTest.kt
index 3baea96990..495e639baf 100644
--- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersPresenterTest.kt
+++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersPresenterTest.kt
@@ -38,6 +38,7 @@ import io.element.android.libraries.usersearch.api.UserSearchResult
import io.element.android.libraries.usersearch.test.FakeUserRepository
import io.element.android.tests.testutils.testCoroutineDispatchers
import kotlinx.collections.immutable.ImmutableList
+import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
import org.junit.Test
@@ -368,7 +369,7 @@ internal class RoomInviteMembersPresenterTest {
}
}
- private fun createDataSource(
+ private fun TestScope.createDataSource(
matrixRoom: MatrixRoom = aMatrixRoom().apply {
givenRoomMembersState(MatrixRoomMembersState.Ready(aRoomMemberList()))
},
diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/RoomMemberListPresenterTests.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/RoomMemberListPresenterTests.kt
index 9625a167f7..b9b52b29f8 100644
--- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/RoomMemberListPresenterTests.kt
+++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/RoomMemberListPresenterTests.kt
@@ -35,6 +35,7 @@ import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
import io.element.android.tests.testutils.testCoroutineDispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
import org.junit.Test
@@ -165,7 +166,7 @@ class RoomMemberListPresenterTests {
}
@ExperimentalCoroutinesApi
-private fun createDataSource(
+private fun TestScope.createDataSource(
matrixRoom: MatrixRoom = aMatrixRoom().apply {
givenRoomMembersState(MatrixRoomMembersState.Ready(aRoomMemberList()))
},
@@ -173,7 +174,7 @@ private fun createDataSource(
) = RoomMemberListDataSource(matrixRoom, coroutineDispatchers)
@ExperimentalCoroutinesApi
-private fun createPresenter(
+private fun TestScope.createPresenter(
matrixRoom: MatrixRoom = FakeMatrixRoom(),
roomMemberListDataSource: RoomMemberListDataSource = createDataSource(),
coroutineDispatchers: CoroutineDispatchers = testCoroutineDispatchers()
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/RoundedIconAtom.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/RoundedIconAtom.kt
new file mode 100644
index 0000000000..752c80175d
--- /dev/null
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/RoundedIconAtom.kt
@@ -0,0 +1,126 @@
+/*
+ * 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.libraries.designsystem.atomic.atoms
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Home
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import io.element.android.libraries.designsystem.preview.ElementPreviewDark
+import io.element.android.libraries.designsystem.preview.ElementPreviewLight
+import io.element.android.libraries.designsystem.theme.LocalColors
+import io.element.android.libraries.designsystem.theme.components.Icon
+
+/**
+ * RoundedIconAtom is an atom which displays an icon inside a rounded container.
+ *
+ * @param modifier the modifier to apply to this layout
+ * @param size the size of the icon
+ * @param resourceId the resource id of the icon to display, exclusive with [imageVector]
+ * @param imageVector the image vector of the icon to display, exclusive with [resourceId]
+ * @param tint the tint to apply to the icon
+ */
+@Composable
+fun RoundedIconAtom(
+ modifier: Modifier = Modifier,
+ size: RoundedIconAtomSize = RoundedIconAtomSize.Large,
+ resourceId: Int? = null,
+ imageVector: ImageVector? = null,
+ tint: Color = MaterialTheme.colorScheme.secondary
+) {
+ Box(
+ modifier = modifier
+ .size(size.toContainerSize())
+ .background(
+ color = LocalColors.current.quinary,
+ shape = RoundedCornerShape(size.toCornerSize())
+ )
+ ) {
+ Icon(
+ modifier = Modifier
+ .align(Alignment.Center)
+ .size(size.toIconSize()),
+ tint = tint,
+ resourceId = resourceId,
+ imageVector = imageVector,
+ contentDescription = "",
+ )
+ }
+}
+
+private fun RoundedIconAtomSize.toContainerSize(): Dp {
+ return when (this) {
+ RoundedIconAtomSize.Medium -> 30.dp
+ RoundedIconAtomSize.Large -> 70.dp
+ }
+}
+
+private fun RoundedIconAtomSize.toCornerSize(): Dp {
+ return when (this) {
+ RoundedIconAtomSize.Medium -> 8.dp
+ RoundedIconAtomSize.Large -> 14.dp
+ }
+}
+
+private fun RoundedIconAtomSize.toIconSize(): Dp {
+ return when (this) {
+ RoundedIconAtomSize.Medium -> 16.dp
+ RoundedIconAtomSize.Large -> 48.dp
+ }
+}
+
+@Preview
+@Composable
+internal fun RoundedIconAtomLightPreview() =
+ ElementPreviewLight { ContentToPreview() }
+
+@Preview
+@Composable
+internal fun RoundedIconAtomDarkPreview() =
+ ElementPreviewDark { ContentToPreview() }
+
+@Composable
+private fun ContentToPreview() {
+ Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
+ RoundedIconAtom(
+ size = RoundedIconAtomSize.Medium,
+ imageVector = Icons.Filled.Home,
+ )
+ RoundedIconAtom(
+ size = RoundedIconAtomSize.Large,
+ imageVector = Icons.Filled.Home,
+ )
+ }
+}
+
+enum class RoundedIconAtomSize {
+ Medium,
+ Large
+}
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/IconTitleSubtitleMolecule.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/IconTitleSubtitleMolecule.kt
index bda10b8ba2..24adf90156 100644
--- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/IconTitleSubtitleMolecule.kt
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/IconTitleSubtitleMolecule.kt
@@ -16,55 +16,55 @@
package io.element.android.libraries.designsystem.atomic.molecules
-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.fillMaxWidth
import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import io.element.android.libraries.designsystem.ElementTextStyles
import io.element.android.libraries.designsystem.R
+import io.element.android.libraries.designsystem.atomic.atoms.RoundedIconAtom
+import io.element.android.libraries.designsystem.atomic.atoms.RoundedIconAtomSize
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
-import io.element.android.libraries.designsystem.theme.LocalColors
-import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.Text
+/**
+ * IconTitleSubtitleMolecule is a molecule which displays an icon, a title and a subtitle.
+ *
+ * @param title the title to display
+ * @param subTitle the subtitle to display
+ * @param modifier the modifier to apply to this layout
+ * @param iconResourceId the resource id of the icon to display, exclusive with [iconImageVector]
+ * @param iconImageVector the image vector of the icon to display, exclusive with [iconResourceId]
+ * @param iconTint the tint to apply to the icon
+ */
@Composable
fun IconTitleSubtitleMolecule(
- iconResourceId: Int,
title: String,
subTitle: String,
modifier: Modifier = Modifier,
+ iconResourceId: Int? = null,
+ iconImageVector: ImageVector? = null,
+ iconTint: Color = MaterialTheme.colorScheme.primary,
) {
Column(modifier) {
- Box(
+ RoundedIconAtom(
modifier = Modifier
- .size(width = 70.dp, height = 70.dp)
- .align(Alignment.CenterHorizontally)
- .background(
- color = LocalColors.current.quinary,
- shape = RoundedCornerShape(14.dp)
- )
- ) {
- Icon(
- modifier = Modifier
- .align(Alignment.Center)
- .size(width = 48.dp, height = 48.dp),
- tint = MaterialTheme.colorScheme.secondary,
- resourceId = iconResourceId,
- contentDescription = "",
- )
- }
+ .align(Alignment.CenterHorizontally),
+ size = RoundedIconAtomSize.Large,
+ resourceId = iconResourceId,
+ imageVector = iconImageVector,
+ tint = iconTint,
+ )
Spacer(modifier = Modifier.height(16.dp))
Text(
text = title,
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/LabelledTextField.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/LabelledTextField.kt
index 81a2ca36fc..71a41c2460 100644
--- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/LabelledTextField.kt
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/LabelledTextField.kt
@@ -37,8 +37,8 @@ fun LabelledTextField(
value: String,
modifier: Modifier = Modifier,
placeholder: String? = null,
- maxLines: Int = Int.MAX_VALUE,
singleLine: Boolean = false,
+ maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE,
onValueChange: (String) -> Unit = {},
) {
Column(
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/async/AsyncFailure.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/async/AsyncFailure.kt
index 5863f4c80c..e81bc140bf 100644
--- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/async/AsyncFailure.kt
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/async/AsyncFailure.kt
@@ -24,12 +24,14 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
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.Text
+import io.element.android.libraries.ui.strings.R as StringR
@Composable
fun AsyncFailure(
@@ -43,11 +45,11 @@ fun AsyncFailure(
.padding(vertical = 32.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
- Text(text = throwable.message ?: "An error occurred")
+ Text(text = throwable.message ?: stringResource(id = StringR.string.error_unknown))
if (onRetry != null) {
Spacer(modifier = Modifier.height(24.dp))
Button(onClick = onRetry) {
- Text(text = "Retry")
+ Text(text = stringResource(id = StringR.string.action_retry))
}
}
}
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/form/TextFieldLocalState.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/form/TextFieldLocalState.kt
index c7366dcfb7..0de4dbba78 100644
--- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/form/TextFieldLocalState.kt
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/form/TextFieldLocalState.kt
@@ -22,5 +22,5 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@Composable
-public fun textFieldState(stateValue: String): MutableState =
+fun textFieldState(stateValue: String): MutableState =
remember(stateValue) { mutableStateOf(stateValue) }
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/PreferenceScreen.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/PreferenceScreen.kt
index 10578ebcca..3826e6ebde 100644
--- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/PreferenceScreen.kt
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/PreferenceScreen.kt
@@ -69,14 +69,11 @@ fun PreferenceView(
)
},
content = {
- val scrollState = rememberScrollState()
Column(
modifier = Modifier
.padding(it)
.consumeWindowInsets(it)
- .verticalScroll(
- state = scrollState,
- )
+ .verticalScroll(state = rememberScrollState())
) {
content()
}
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/Icon.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/Icon.kt
index dedf2af060..24de433058 100644
--- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/Icon.kt
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/Icon.kt
@@ -30,6 +30,54 @@ import androidx.compose.ui.tooling.preview.Preview
import io.element.android.libraries.designsystem.preview.ElementThemedPreview
import io.element.android.libraries.designsystem.preview.PreviewGroup
+/**
+ * Icon is a wrapper around [androidx.compose.material3.Icon] which allows to use
+ * [ImageVector], [ImageBitmap] or [DrawableRes] as icon source.
+ *
+ * @param contentDescription the content description to be used for accessibility
+ * @param modifier the modifier to apply to this layout
+ * @param tint the tint to apply to the icon
+ * @param imageVector the image vector of the icon to display, exclusive with [bitmap] and [resourceId]
+ * @param bitmap the bitmap of the icon to display, exclusive with [imageVector] and [resourceId]
+ * @param resourceId the resource id of the icon to display, exclusive with [imageVector] and [bitmap]
+ */
+@Composable
+fun Icon(
+ contentDescription: String?,
+ modifier: Modifier = Modifier,
+ tint: Color = LocalContentColor.current,
+ imageVector: ImageVector? = null,
+ bitmap: ImageBitmap? = null,
+ @DrawableRes resourceId: Int? = null,
+) {
+ when {
+ imageVector != null -> {
+ Icon(
+ imageVector = imageVector,
+ contentDescription = contentDescription,
+ modifier = modifier,
+ tint = tint
+ )
+ }
+ bitmap != null -> {
+ Icon(
+ bitmap = bitmap,
+ contentDescription = contentDescription,
+ modifier = modifier,
+ tint = tint
+ )
+ }
+ resourceId != null -> {
+ Icon(
+ resourceId = resourceId,
+ contentDescription = contentDescription,
+ modifier = modifier,
+ tint = tint
+ )
+ }
+ }
+}
+
@Composable
fun Icon(
imageVector: ImageVector,
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/OutlinedTextField.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/OutlinedTextField.kt
index 423d1e46d1..358ca2abab 100644
--- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/OutlinedTextField.kt
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/OutlinedTextField.kt
@@ -67,7 +67,7 @@ fun OutlinedTextField(
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
keyboardActions: KeyboardActions = KeyboardActions.Default,
singleLine: Boolean = false,
- maxLines: Int = Int.MAX_VALUE,
+ maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
shape: Shape = OutlinedTextFieldDefaults.shape,
colors: TextFieldColors = OutlinedTextFieldDefaults.colors()
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/TextField.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/TextField.kt
index 8376369fba..2ad41887aa 100644
--- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/TextField.kt
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/TextField.kt
@@ -68,7 +68,7 @@ fun TextField(
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
keyboardActions: KeyboardActions = KeyboardActions.Default,
singleLine: Boolean = false,
- maxLines: Int = Int.MAX_VALUE,
+ maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
shape: Shape = TextFieldDefaults.shape,
colors: TextFieldColors = TextFieldDefaults.colors()
diff --git a/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/StateContentFormatter.kt b/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/StateContentFormatter.kt
index 169ec55a9d..ccf9c2976a 100644
--- a/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/StateContentFormatter.kt
+++ b/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/StateContentFormatter.kt
@@ -22,6 +22,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.StateContent
import io.element.android.services.toolbox.api.strings.StringProvider
import timber.log.Timber
import javax.inject.Inject
+import io.element.android.libraries.ui.strings.R as StringR
class StateContentFormatter @Inject constructor(
private val sp: StringProvider,
@@ -49,7 +50,7 @@ class StateContentFormatter @Inject constructor(
sp.getString(R.string.state_event_room_created, senderDisplayName)
}
}
- is OtherState.RoomEncryption -> sp.getString(io.element.android.libraries.ui.strings.R.string.common_encryption_enabled)
+ is OtherState.RoomEncryption -> sp.getString(StringR.string.common_encryption_enabled)
is OtherState.RoomName -> {
val hasRoomName = content.name != null
when {
diff --git a/libraries/testtags/src/main/kotlin/io/element/android/libraries/testtags/TestTags.kt b/libraries/testtags/src/main/kotlin/io/element/android/libraries/testtags/TestTags.kt
index df12c755e3..d702c797a8 100644
--- a/libraries/testtags/src/main/kotlin/io/element/android/libraries/testtags/TestTags.kt
+++ b/libraries/testtags/src/main/kotlin/io/element/android/libraries/testtags/TestTags.kt
@@ -37,7 +37,6 @@ object TestTags {
* Change server screen.
*/
val changeServerServer = TestTag("change_server-server")
- val changeServerContinue = TestTag("change_server-continue")
/**
* Room list / Home screen.
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 daffb59602..0f2affe054 100644
--- a/libraries/ui-strings/src/main/res/values-cs/translations.xml
+++ b/libraries/ui-strings/src/main/res/values-cs/translations.xml
@@ -150,12 +150,6 @@
"Nahrání média se nezdařilo, zkuste to prosím znovu."
"Nahrání média se nezdařilo, zkuste to prosím znovu."
"Zaškrtněte, pokud chcete skrýt všechny aktuální a budoucí zprávy od tohoto uživatele"
- "Změnit poskytovatele účtu"
- "Soukromý server pro zaměstnance Elementu."
- "Matrix je otevřená síť pro bezpečnou a decentralizovanou komunikaci."
- "Zde budou uloženy vaše konverzace - podobně jako u poskytovatele e-mailových služeb uchováváte své e-maily."
- "Chystáte se přihlásit do služby %1$s"
- "Chystáte se vytvořit účet na %1$s"
"Rageshake"
"Práh detekce"
"Obecné"
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 90f27bd8dd..ad4c3571ad 100644
--- a/libraries/ui-strings/src/main/res/values-de/translations.xml
+++ b/libraries/ui-strings/src/main/res/values-de/translations.xml
@@ -142,9 +142,6 @@
"Medienauswahl fehlgeschlagen, bitte versuche es erneut."
"Medien hochladen fehlgeschlagen. Bitte versuchen Sie es erneut."
"Prüfe, ob du alle aktuellen und zukünftigen Nachrichten dieses Benutzers ausblenden möchtest"
- "Kontoanbieter wechseln"
- "Ein privater Server für Element-Mitarbeiter."
- "Matrix ist ein offenes Netzwerk für sichere, dezentrale Kommunikation"
"Rageshake"
"Erkennungsschwelle"
"Allgemein"
@@ -156,4 +153,4 @@
"Sie können alle unsere Nutzerbedingungen %1$s lesen."
"hier"
"Nutzer blockieren"
-
\ No newline at end of file
+
diff --git a/libraries/ui-strings/src/main/res/values/localazy.xml b/libraries/ui-strings/src/main/res/values/localazy.xml
index 43d19f2202..93d2cc289e 100644
--- a/libraries/ui-strings/src/main/res/values/localazy.xml
+++ b/libraries/ui-strings/src/main/res/values/localazy.xml
@@ -57,9 +57,11 @@
"View Source"
"Yes"
"About"
+ "Acceptable use policy"
"Analytics"
"Audio"
"Bubbles"
+ "Copyright"
"Creating room…"
"Left room"
"Decryption error"
@@ -81,11 +83,13 @@
"Message layout"
"Message removed"
"Modern"
+ "Mute"
"No results"
"Offline"
"Password"
"People"
"Permalink"
+ "Privacy policy"
"Reactions"
"Replying to %1$s"
"Report a bug"
@@ -104,11 +108,13 @@
"Sticker"
"Success"
"Suggestions"
+ "Third-party notices"
"Topic"
"What is this room about?"
"Unable to decrypt"
"We were unable to successfully send invites to one or more users."
"Unable to send invite(s)"
+ "Unmute"
"Unsupported event"
"Username"
"Verification cancelled"
@@ -164,12 +170,6 @@
"Failed processing media to upload, please try again."
"Failed uploading media, please try again."
"Check if you want to hide all current and future messages from this user"
- "Change account provider"
- "A private server for Element employees."
- "Matrix is an open network for secure, decentralised communication."
- "This is where your conversations will live — just like you would use an email provider to keep your emails."
- "You’re about to sign in to %1$s"
- "You’re about to create an account on %1$s"
"Rageshake"
"Detection threshold"
"General"
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 9d9971a842..54bef4652b 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
@@ -17,11 +17,13 @@
package io.element.android.samples.minimal
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.oidc.customtab.DefaultOidcActionFlow
-import io.element.android.features.login.impl.root.LoginRootPresenter
-import io.element.android.features.login.impl.root.LoginRootView
+import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
+import io.element.android.features.login.impl.screens.loginpassword.LoginPasswordPresenter
+import io.element.android.features.login.impl.screens.loginpassword.LoginPasswordView
+import io.element.android.features.login.impl.util.defaultAccountProvider
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
class LoginScreen(private val authenticationService: MatrixAuthenticationService) {
@@ -29,13 +31,18 @@ class LoginScreen(private val authenticationService: MatrixAuthenticationService
@Composable
fun Content(modifier: Modifier = Modifier) {
val presenter = remember {
- LoginRootPresenter(
+ LoginPasswordPresenter(
authenticationService = authenticationService,
- DefaultOidcActionFlow()
+ AccountProviderDataSource()
)
}
+
+ LaunchedEffect(Unit) {
+ authenticationService.setHomeserver(defaultAccountProvider.title)
+ }
+
val state = presenter.present()
- LoginRootView(
+ LoginPasswordView(
state = state,
modifier = modifier,
onBackPressed = {},
diff --git a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/TestCoroutineDispatchers.kt b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/TestCoroutineDispatchers.kt
index 4628f4910c..786d8f8cd2 100644
--- a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/TestCoroutineDispatchers.kt
+++ b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/TestCoroutineDispatchers.kt
@@ -21,19 +21,16 @@ package io.element.android.tests.testutils
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.StandardTestDispatcher
-import kotlinx.coroutines.test.TestCoroutineScheduler
+import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.UnconfinedTestDispatcher
/**
* Create a [CoroutineDispatchers] instance for testing.
*
- * @param testScheduler The [TestCoroutineScheduler] to use. If using [runTest] use the one provided by its [TestScope].
- * If null the [TestDispatcher] logic will select one or create a new one.
* @param useUnconfinedTestDispatcher If true, use [UnconfinedTestDispatcher] for all dispatchers.
* If false, use [StandardTestDispatcher] for all dispatchers.
*/
-fun testCoroutineDispatchers(
- testScheduler: TestCoroutineScheduler? = null,
+fun TestScope.testCoroutineDispatchers(
useUnconfinedTestDispatcher: Boolean = true,
): CoroutineDispatchers = when (useUnconfinedTestDispatcher) {
false -> CoroutineDispatchers(
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.accountprovider_null_DefaultGroup_AccountProviderViewDarkPreview_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.accountprovider_null_DefaultGroup_AccountProviderViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..a0ea87cbc0
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.accountprovider_null_DefaultGroup_AccountProviderViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:c5ecc3788746bc425efe843018b13a8fad0a10665fcc0b9b528df6df54791cef
+size 20811
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.accountprovider_null_DefaultGroup_AccountProviderViewDarkPreview_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.accountprovider_null_DefaultGroup_AccountProviderViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..2b5edb0440
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.accountprovider_null_DefaultGroup_AccountProviderViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:2036b2b9b3815c69dca77fbde216be6d333607d6dc3ed254923ba35244d20061
+size 9441
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.accountprovider_null_DefaultGroup_AccountProviderViewDarkPreview_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.accountprovider_null_DefaultGroup_AccountProviderViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..22503a7c95
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.accountprovider_null_DefaultGroup_AccountProviderViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:59d342f4e163fd52199a2e05a2d403698a87bb51ce2494ee52c01968edbd71d0
+size 10482
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.accountprovider_null_DefaultGroup_AccountProviderViewDarkPreview_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.accountprovider_null_DefaultGroup_AccountProviderViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..1f570432a2
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.accountprovider_null_DefaultGroup_AccountProviderViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:0e38d29272030a66ce7d581f82f82553a3a19f4694117e1b5dd5d507ecce20c4
+size 8532
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.accountprovider_null_DefaultGroup_AccountProviderViewDarkPreview_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.accountprovider_null_DefaultGroup_AccountProviderViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..218db727b5
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.accountprovider_null_DefaultGroup_AccountProviderViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:07414bddb30f4535ba56c38cd295cc272a8531641485a5339f898b4186d3a466
+size 7464
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.accountprovider_null_DefaultGroup_AccountProviderViewLightPreview_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.accountprovider_null_DefaultGroup_AccountProviderViewLightPreview_0_null_0,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..d44b573a9a
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.accountprovider_null_DefaultGroup_AccountProviderViewLightPreview_0_null_0,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:6ce4df1f9faf5704b2407357f06a8879ad57fcc593a0fa59d642f580f1bd7bab
+size 20098
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.accountprovider_null_DefaultGroup_AccountProviderViewLightPreview_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.accountprovider_null_DefaultGroup_AccountProviderViewLightPreview_0_null_1,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..37e177fe42
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.accountprovider_null_DefaultGroup_AccountProviderViewLightPreview_0_null_1,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:64ed033fbcccc2a5d40664ebbe2a32782579f77eaa07200a329d49165f2606a2
+size 9122
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.accountprovider_null_DefaultGroup_AccountProviderViewLightPreview_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.accountprovider_null_DefaultGroup_AccountProviderViewLightPreview_0_null_2,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..bc67803f7f
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.accountprovider_null_DefaultGroup_AccountProviderViewLightPreview_0_null_2,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:09ce6b3272ea2450b30c7b3b5fe6ee0aef44a03e62f433f42ab17b255e2fa680
+size 10172
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.accountprovider_null_DefaultGroup_AccountProviderViewLightPreview_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.accountprovider_null_DefaultGroup_AccountProviderViewLightPreview_0_null_3,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..534b7f86b3
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.accountprovider_null_DefaultGroup_AccountProviderViewLightPreview_0_null_3,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:d9ac9e05b076079caaaf9cd097b75d6cb928982a7c24a6f2eb9c73573521d959
+size 8276
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.accountprovider_null_DefaultGroup_AccountProviderViewLightPreview_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.accountprovider_null_DefaultGroup_AccountProviderViewLightPreview_0_null_4,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..e0f3550333
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.accountprovider_null_DefaultGroup_AccountProviderViewLightPreview_0_null_4,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:37c6d5fd1dbbb10fe4d18bb70c27409da86d832e3394d05841f194729c5b14cd
+size 7148
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.changeserver_null_DefaultGroup_ChangeServerViewDarkPreview_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.changeserver_null_DefaultGroup_ChangeServerViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png
index 55945f00cb..665c8811ac 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.changeserver_null_DefaultGroup_ChangeServerViewDarkPreview_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.changeserver_null_DefaultGroup_ChangeServerViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:ac39dda453fbdf0f5cc3c6df2d9436a171c141c8be2ca2e2d2a2c7ee5d16b36f
-size 39650
+oid sha256:bb0d3bfcfd75cbd75fd9270ff1dc27090e5dbac79ca8db8a46d91a4c12bc966b
+size 4457
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.changeserver_null_DefaultGroup_ChangeServerViewDarkPreview_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.changeserver_null_DefaultGroup_ChangeServerViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png
deleted file mode 100644
index 9f65a20aaa..0000000000
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.changeserver_null_DefaultGroup_ChangeServerViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:49ef94a7bd404fe5982f7319aa1c4f6b5d29d11675f53e192af85b4efb8c3517
-size 43267
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.changeserver_null_DefaultGroup_ChangeServerViewDarkPreview_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.changeserver_null_DefaultGroup_ChangeServerViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png
deleted file mode 100644
index 75c156af00..0000000000
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.changeserver_null_DefaultGroup_ChangeServerViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:47942f38dd2afb38d0e1174bd155bb708f645b231b250525920f5abaa7074308
-size 43217
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.changeserver_null_DefaultGroup_ChangeServerViewDarkPreview_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.changeserver_null_DefaultGroup_ChangeServerViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png
deleted file mode 100644
index bfba913061..0000000000
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.changeserver_null_DefaultGroup_ChangeServerViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:b006f7768bbc48cf68e5d897920a14894496d0b2b84a1839a7158b45edea9a75
-size 48757
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.changeserver_null_DefaultGroup_ChangeServerViewDarkPreview_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.changeserver_null_DefaultGroup_ChangeServerViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png
deleted file mode 100644
index a63cf4d7e8..0000000000
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.changeserver_null_DefaultGroup_ChangeServerViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:12702b2d71193e376f15df6f1a029531868b9651b70c40d2d738646f15980704
-size 42362
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.changeserver_null_DefaultGroup_ChangeServerViewDarkPreview_0_null_5,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.changeserver_null_DefaultGroup_ChangeServerViewDarkPreview_0_null_5,NEXUS_5,1.0,en].png
deleted file mode 100644
index 7b72ffcde5..0000000000
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.changeserver_null_DefaultGroup_ChangeServerViewDarkPreview_0_null_5,NEXUS_5,1.0,en].png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:a942ed49efc175776994cf58d511af3be8f878b30ed5b34ab3ae0d600f2e0db1
-size 42316
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.changeserver_null_DefaultGroup_ChangeServerViewLightPreview_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.changeserver_null_DefaultGroup_ChangeServerViewLightPreview_0_null_0,NEXUS_5,1.0,en].png
index 8c716007cc..665c8811ac 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.changeserver_null_DefaultGroup_ChangeServerViewLightPreview_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.changeserver_null_DefaultGroup_ChangeServerViewLightPreview_0_null_0,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:d692cd9030e6193691e676de62f68a9241cebf220a1f6c7986e095370e25c547
-size 37839
+oid sha256:bb0d3bfcfd75cbd75fd9270ff1dc27090e5dbac79ca8db8a46d91a4c12bc966b
+size 4457
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.changeserver_null_DefaultGroup_ChangeServerViewLightPreview_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.changeserver_null_DefaultGroup_ChangeServerViewLightPreview_0_null_1,NEXUS_5,1.0,en].png
deleted file mode 100644
index c979df7432..0000000000
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.changeserver_null_DefaultGroup_ChangeServerViewLightPreview_0_null_1,NEXUS_5,1.0,en].png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:716e20f80dd634feb58ee26247defdd8e390d268e9ec5d9767ad7b574d7ca2bc
-size 41581
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.changeserver_null_DefaultGroup_ChangeServerViewLightPreview_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.changeserver_null_DefaultGroup_ChangeServerViewLightPreview_0_null_2,NEXUS_5,1.0,en].png
deleted file mode 100644
index bc159e7bcb..0000000000
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.changeserver_null_DefaultGroup_ChangeServerViewLightPreview_0_null_2,NEXUS_5,1.0,en].png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:9658c925b80565109772111972a495d437bda9ae97a9d9327d92a44ff0a1b9d1
-size 41521
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.changeserver_null_DefaultGroup_ChangeServerViewLightPreview_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.changeserver_null_DefaultGroup_ChangeServerViewLightPreview_0_null_3,NEXUS_5,1.0,en].png
deleted file mode 100644
index d0764f07b3..0000000000
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.changeserver_null_DefaultGroup_ChangeServerViewLightPreview_0_null_3,NEXUS_5,1.0,en].png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:f0511bf277f5194a6d38cf13416c2b2a1c834bf216c511243351d2f9bfe1b7e3
-size 47857
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.changeserver_null_DefaultGroup_ChangeServerViewLightPreview_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.changeserver_null_DefaultGroup_ChangeServerViewLightPreview_0_null_4,NEXUS_5,1.0,en].png
deleted file mode 100644
index be3729d669..0000000000
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.changeserver_null_DefaultGroup_ChangeServerViewLightPreview_0_null_4,NEXUS_5,1.0,en].png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:e248fa1785749f3a590576ed6f3c6899ad0042f71a80a4afdc7336981c7419a3
-size 40589
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.changeserver_null_DefaultGroup_ChangeServerViewLightPreview_0_null_5,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.changeserver_null_DefaultGroup_ChangeServerViewLightPreview_0_null_5,NEXUS_5,1.0,en].png
deleted file mode 100644
index 7a621153ce..0000000000
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.changeserver_null_DefaultGroup_ChangeServerViewLightPreview_0_null_5,NEXUS_5,1.0,en].png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:903df8ff5d3816a85e16a3ec46b2a98e742ba0fc0a317df829b0fca57f33d9aa
-size 40553
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenDarkPreview_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.root_null_DefaultGroup_LoginRootScreenDarkPreview_0_null_0,NEXUS_5,1.0,en].png
deleted file mode 100644
index 22b1ce6d04..0000000000
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenDarkPreview_0_null_0,NEXUS_5,1.0,en].png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:558f8f3b34e3a94a62460cca37396f94b0caa8197b7fbd49ce894382392f1899
-size 31512
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenDarkPreview_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.root_null_DefaultGroup_LoginRootScreenDarkPreview_0_null_1,NEXUS_5,1.0,en].png
deleted file mode 100644
index 22b1ce6d04..0000000000
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenDarkPreview_0_null_1,NEXUS_5,1.0,en].png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:558f8f3b34e3a94a62460cca37396f94b0caa8197b7fbd49ce894382392f1899
-size 31512
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenDarkPreview_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.root_null_DefaultGroup_LoginRootScreenDarkPreview_0_null_2,NEXUS_5,1.0,en].png
deleted file mode 100644
index f48af1a990..0000000000
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenDarkPreview_0_null_2,NEXUS_5,1.0,en].png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:c4469faa61e68d951ed13b9cf324d11a99a1b041fb096bade2cd5d11c8185acc
-size 33232
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenDarkPreview_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.root_null_DefaultGroup_LoginRootScreenDarkPreview_0_null_3,NEXUS_5,1.0,en].png
deleted file mode 100644
index d65333610e..0000000000
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenDarkPreview_0_null_3,NEXUS_5,1.0,en].png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:81125c72229ddb56f3c3e8b826de42f947f8739c3f372cad99917ee7d4db3b5c
-size 33318
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenDarkPreview_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.root_null_DefaultGroup_LoginRootScreenDarkPreview_0_null_4,NEXUS_5,1.0,en].png
deleted file mode 100644
index 4efbb4fca6..0000000000
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenDarkPreview_0_null_4,NEXUS_5,1.0,en].png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:cff519b372c22b623f72407e7c4113cfa24dae31fc517aea5de75bea9b4d61bf
-size 29616
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenDarkPreview_0_null_5,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.root_null_DefaultGroup_LoginRootScreenDarkPreview_0_null_5,NEXUS_5,1.0,en].png
deleted file mode 100644
index f48af1a990..0000000000
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenDarkPreview_0_null_5,NEXUS_5,1.0,en].png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:c4469faa61e68d951ed13b9cf324d11a99a1b041fb096bade2cd5d11c8185acc
-size 33232
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenDarkPreview_0_null_6,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.root_null_DefaultGroup_LoginRootScreenDarkPreview_0_null_6,NEXUS_5,1.0,en].png
deleted file mode 100644
index 1a305e0c79..0000000000
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenDarkPreview_0_null_6,NEXUS_5,1.0,en].png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:78b278d94f46995b7d11719677b72c0804219b1c36072fd8e2ad6dfdb9de8aba
-size 25313
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenDarkPreview_0_null_7,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.root_null_DefaultGroup_LoginRootScreenDarkPreview_0_null_7,NEXUS_5,1.0,en].png
deleted file mode 100644
index 2adab63ade..0000000000
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenDarkPreview_0_null_7,NEXUS_5,1.0,en].png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:25b6b15bd1646316a6b1dde8d68396e6b8f1e677b0528f07cab8f8d17a828902
-size 24564
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenDarkPreview_0_null_8,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.root_null_DefaultGroup_LoginRootScreenDarkPreview_0_null_8,NEXUS_5,1.0,en].png
deleted file mode 100644
index b31cc0a315..0000000000
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenDarkPreview_0_null_8,NEXUS_5,1.0,en].png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:a59f9b56497f976a0664855e3da6245c423e8608c24b3f08b0c0cef0809ba0ee
-size 20273
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenDarkPreview_0_null_9,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.root_null_DefaultGroup_LoginRootScreenDarkPreview_0_null_9,NEXUS_5,1.0,en].png
deleted file mode 100644
index 873614913b..0000000000
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenDarkPreview_0_null_9,NEXUS_5,1.0,en].png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:1f8841cada0e8826a43cbca6ed8af3aa703633eccdcb664be3377ab6b38ed960
-size 26073
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenLightPreview_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.root_null_DefaultGroup_LoginRootScreenLightPreview_0_null_0,NEXUS_5,1.0,en].png
deleted file mode 100644
index a9d3b597b1..0000000000
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenLightPreview_0_null_0,NEXUS_5,1.0,en].png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:ef34f0de8599a2c950690d1ef1d4c8f68419b137594177ed09cfe3715deb3767
-size 30490
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenLightPreview_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.root_null_DefaultGroup_LoginRootScreenLightPreview_0_null_1,NEXUS_5,1.0,en].png
deleted file mode 100644
index a9d3b597b1..0000000000
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenLightPreview_0_null_1,NEXUS_5,1.0,en].png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:ef34f0de8599a2c950690d1ef1d4c8f68419b137594177ed09cfe3715deb3767
-size 30490
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenLightPreview_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.root_null_DefaultGroup_LoginRootScreenLightPreview_0_null_2,NEXUS_5,1.0,en].png
deleted file mode 100644
index 718f68a9f8..0000000000
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenLightPreview_0_null_2,NEXUS_5,1.0,en].png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:99de01c644442e4d309b1d6bc75d0920eb205df31ef0c0f2411cbb756e0a838e
-size 32106
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenLightPreview_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.root_null_DefaultGroup_LoginRootScreenLightPreview_0_null_3,NEXUS_5,1.0,en].png
deleted file mode 100644
index a11d8fe9d2..0000000000
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenLightPreview_0_null_3,NEXUS_5,1.0,en].png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:acdde4f931b9e8aa18280f8f6f6e003ba406801ce1603e87ab087afe44465167
-size 32124
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenLightPreview_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.root_null_DefaultGroup_LoginRootScreenLightPreview_0_null_4,NEXUS_5,1.0,en].png
deleted file mode 100644
index 5b2872a1e2..0000000000
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenLightPreview_0_null_4,NEXUS_5,1.0,en].png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:6f3cbb18c3c2b766b7b10b204edc300cce73193c458a2c9b7dd88bf6fa7bcd6b
-size 29007
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenLightPreview_0_null_5,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.root_null_DefaultGroup_LoginRootScreenLightPreview_0_null_5,NEXUS_5,1.0,en].png
deleted file mode 100644
index 718f68a9f8..0000000000
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenLightPreview_0_null_5,NEXUS_5,1.0,en].png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:99de01c644442e4d309b1d6bc75d0920eb205df31ef0c0f2411cbb756e0a838e
-size 32106
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenLightPreview_0_null_6,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.root_null_DefaultGroup_LoginRootScreenLightPreview_0_null_6,NEXUS_5,1.0,en].png
deleted file mode 100644
index b7201b0739..0000000000
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenLightPreview_0_null_6,NEXUS_5,1.0,en].png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:3a1473e95cf2436f5e42fb1ca2d7bbd40915be830f012b96ad47801ee1fd70a2
-size 24580
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenLightPreview_0_null_7,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.root_null_DefaultGroup_LoginRootScreenLightPreview_0_null_7,NEXUS_5,1.0,en].png
deleted file mode 100644
index b20d358a12..0000000000
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenLightPreview_0_null_7,NEXUS_5,1.0,en].png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:e338c8c2d3f89f3b7fdeafc5afffb36399855155e2b85231349e13e381e4cf1f
-size 23617
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenLightPreview_0_null_8,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.root_null_DefaultGroup_LoginRootScreenLightPreview_0_null_8,NEXUS_5,1.0,en].png
deleted file mode 100644
index 4313439757..0000000000
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenLightPreview_0_null_8,NEXUS_5,1.0,en].png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:dea12dfe1c1799b1d2d765214b1bd911154a19a7794bf37c6663d179cec76c81
-size 19556
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenLightPreview_0_null_9,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.root_null_DefaultGroup_LoginRootScreenLightPreview_0_null_9,NEXUS_5,1.0,en].png
deleted file mode 100644
index ed81ad1335..0000000000
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenLightPreview_0_null_9,NEXUS_5,1.0,en].png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:1dc7d39a9b4e0f3eee8157b0290146b6bae5e6d0ea280296ddfc7051f31b95df
-size 24786
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.screens.changeaccountprovider_null_DefaultGroup_ChangeAccountProviderViewDarkPreview_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.changeaccountprovider_null_DefaultGroup_ChangeAccountProviderViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..d7f8ebf657
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.screens.changeaccountprovider_null_DefaultGroup_ChangeAccountProviderViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:0cf16abe33c78fbe4db6c447fb54a4c6f4495e02ab13774ddd25bc74c24e785b
+size 43758
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.screens.changeaccountprovider_null_DefaultGroup_ChangeAccountProviderViewLightPreview_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.changeaccountprovider_null_DefaultGroup_ChangeAccountProviderViewLightPreview_0_null_0,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..cd94f38e06
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.screens.changeaccountprovider_null_DefaultGroup_ChangeAccountProviderViewLightPreview_0_null_0,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:3712ed6d166f726d4c6f56cdf9208787e7b79aec49900e3e59702e23da07c076
+size 43460
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.screens.confirmaccountprovider_null_DefaultGroup_ConfirmAccountProviderViewDarkPreview_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.confirmaccountprovider_null_DefaultGroup_ConfirmAccountProviderViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..406ee49b58
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.screens.confirmaccountprovider_null_DefaultGroup_ConfirmAccountProviderViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:51897c460fad147f358268ad6b15d6865b6378edd8533d744fe108bc96f5d718
+size 40107
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.screens.confirmaccountprovider_null_DefaultGroup_ConfirmAccountProviderViewLightPreview_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.confirmaccountprovider_null_DefaultGroup_ConfirmAccountProviderViewLightPreview_0_null_0,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..3346d30e90
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.screens.confirmaccountprovider_null_DefaultGroup_ConfirmAccountProviderViewLightPreview_0_null_0,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:57ace1181dd3d95bdbc0ec11270951b6b2fce6301a68af88c6cddc428acba022
+size 39533
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.screens.loginpassword_null_DefaultGroup_LoginPasswordViewDarkPreview_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.loginpassword_null_DefaultGroup_LoginPasswordViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..4fc7c6de68
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.screens.loginpassword_null_DefaultGroup_LoginPasswordViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:84a19a75922a661d173d1213797112bba2e8faf5093561f336925c2f06ecd17a
+size 31799
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.screens.loginpassword_null_DefaultGroup_LoginPasswordViewDarkPreview_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.loginpassword_null_DefaultGroup_LoginPasswordViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..9dcfbbebaa
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.screens.loginpassword_null_DefaultGroup_LoginPasswordViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:20a03449955f95720f3ef1fcf36d9fd82fb34c5e2efea7515e1dd25b11a5257b
+size 31834
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.screens.loginpassword_null_DefaultGroup_LoginPasswordViewDarkPreview_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.loginpassword_null_DefaultGroup_LoginPasswordViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..4fc7c6de68
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.screens.loginpassword_null_DefaultGroup_LoginPasswordViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:84a19a75922a661d173d1213797112bba2e8faf5093561f336925c2f06ecd17a
+size 31799
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.screens.loginpassword_null_DefaultGroup_LoginPasswordViewLightPreview_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.loginpassword_null_DefaultGroup_LoginPasswordViewLightPreview_0_null_0,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..c9c94638f6
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.screens.loginpassword_null_DefaultGroup_LoginPasswordViewLightPreview_0_null_0,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:367400d1ff6e9e39a5a27bcd45227c75a13fc3d2a81ca532eda75a07481c025a
+size 31472
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.screens.loginpassword_null_DefaultGroup_LoginPasswordViewLightPreview_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.loginpassword_null_DefaultGroup_LoginPasswordViewLightPreview_0_null_1,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..0ea591d06f
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.screens.loginpassword_null_DefaultGroup_LoginPasswordViewLightPreview_0_null_1,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:d33b86a10b237ee11b7fcf757462c9685f2cc10fb30ee1c76f2808306b7bc14c
+size 31456
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.screens.loginpassword_null_DefaultGroup_LoginPasswordViewLightPreview_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.loginpassword_null_DefaultGroup_LoginPasswordViewLightPreview_0_null_2,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..c9c94638f6
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.screens.loginpassword_null_DefaultGroup_LoginPasswordViewLightPreview_0_null_2,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:367400d1ff6e9e39a5a27bcd45227c75a13fc3d2a81ca532eda75a07481c025a
+size 31472
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.screens.searchaccountprovider_null_DefaultGroup_SearchAccountProviderViewDarkPreview_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.searchaccountprovider_null_DefaultGroup_SearchAccountProviderViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..74b6c3556e
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.screens.searchaccountprovider_null_DefaultGroup_SearchAccountProviderViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:f0456d54912f117e8d07c3023c4a0c10fc4933234937f20a87c79cdc8364c3d1
+size 26891
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.screens.searchaccountprovider_null_DefaultGroup_SearchAccountProviderViewDarkPreview_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.searchaccountprovider_null_DefaultGroup_SearchAccountProviderViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..02296060db
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.screens.searchaccountprovider_null_DefaultGroup_SearchAccountProviderViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:ac1a1bec7cc71e7d1ab8513c98cb315f720fe0a7341e3485fabbaff4134352aa
+size 48795
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.screens.searchaccountprovider_null_DefaultGroup_SearchAccountProviderViewLightPreview_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.searchaccountprovider_null_DefaultGroup_SearchAccountProviderViewLightPreview_0_null_0,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..0facef8667
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.screens.searchaccountprovider_null_DefaultGroup_SearchAccountProviderViewLightPreview_0_null_0,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:62d743d3bfa2744891c6cd2bb4052cccb623b9ff6e99c06ce07948c71212cc54
+size 26598
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.screens.searchaccountprovider_null_DefaultGroup_SearchAccountProviderViewLightPreview_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.searchaccountprovider_null_DefaultGroup_SearchAccountProviderViewLightPreview_0_null_1,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..95741de0ba
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.screens.searchaccountprovider_null_DefaultGroup_SearchAccountProviderViewLightPreview_0_null_1,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:4935ca32e03ea0687d766f6675af3fb78ee7cfff2c7e32e3557d1065fc91ad40
+size 48404
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.verifysession.impl_null_DefaultGroup_VerifySelfSessionViewDarkPreview_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.verifysession.impl_null_DefaultGroup_VerifySelfSessionViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png
index f67e775a44..9e0556cea5 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.verifysession.impl_null_DefaultGroup_VerifySelfSessionViewDarkPreview_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.verifysession.impl_null_DefaultGroup_VerifySelfSessionViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:eed20ed1221b53520bcc92c76a7b45f19134d5896220c109f89914d0d2de8c68
-size 29536
+oid sha256:64e2e959463e616c85dbf84f7e89bf4921ad8bda8b56c3ad5923c97d41ebb17a
+size 29109
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.verifysession.impl_null_DefaultGroup_VerifySelfSessionViewDarkPreview_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.verifysession.impl_null_DefaultGroup_VerifySelfSessionViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png
index 2980892672..8e27acb63e 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.verifysession.impl_null_DefaultGroup_VerifySelfSessionViewDarkPreview_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.verifysession.impl_null_DefaultGroup_VerifySelfSessionViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:bff3aaf96ea36cd876e0bb262155f7da0a5e110ab5308872bf43d80fa50da414
-size 26909
+oid sha256:b29de0962ec154558d45759678ae01b4cd79b5b0ca77e44023fa8d4c461375a7
+size 26436
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.verifysession.impl_null_DefaultGroup_VerifySelfSessionViewDarkPreview_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.verifysession.impl_null_DefaultGroup_VerifySelfSessionViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png
index de5e56864b..484bf3737d 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.verifysession.impl_null_DefaultGroup_VerifySelfSessionViewDarkPreview_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.verifysession.impl_null_DefaultGroup_VerifySelfSessionViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:a1d4d57ed7cd0ab3c274d0cb46d70f6a2821fcf7fb2b4350258e675ecad8c964
-size 61153
+oid sha256:c1f43f5652bc1312b0a10b074ef7cf1b630774c9633551da36cf563218cd3812
+size 60953
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.verifysession.impl_null_DefaultGroup_VerifySelfSessionViewDarkPreview_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.verifysession.impl_null_DefaultGroup_VerifySelfSessionViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png
index 35dbc4a40e..a26fee1b27 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.verifysession.impl_null_DefaultGroup_VerifySelfSessionViewDarkPreview_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.verifysession.impl_null_DefaultGroup_VerifySelfSessionViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:e5db9cb1ebafb13c52b0e5f68c7dee3d6d27ce8b9bad0a1f5dd5ce76f4bda4cb
-size 61738
+oid sha256:cc770343fe1da082c3f65d6f51f4b69e62751807a5b9217a0c785d720789dad2
+size 61539
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.verifysession.impl_null_DefaultGroup_VerifySelfSessionViewDarkPreview_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.verifysession.impl_null_DefaultGroup_VerifySelfSessionViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png
index abbfcf77d5..d034c99ff3 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.verifysession.impl_null_DefaultGroup_VerifySelfSessionViewDarkPreview_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.verifysession.impl_null_DefaultGroup_VerifySelfSessionViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:102bc04b2e12eb68919c45346f4aab946470e8c9e9848ed59a03ad07b178e1bb
-size 32639
+oid sha256:e2c28114a14e7251b70254516bb454d3cf05a183f3b5dcec9a8a8c2913d4d80e
+size 32441
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.verifysession.impl_null_DefaultGroup_VerifySelfSessionViewDarkPreview_0_null_5,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.verifysession.impl_null_DefaultGroup_VerifySelfSessionViewDarkPreview_0_null_5,NEXUS_5,1.0,en].png
index c5f846756d..04a4cd408d 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.verifysession.impl_null_DefaultGroup_VerifySelfSessionViewDarkPreview_0_null_5,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.verifysession.impl_null_DefaultGroup_VerifySelfSessionViewDarkPreview_0_null_5,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:052f915a61028b2cd9605c07fd13000b09f003744397740cfee7054dbcabdcbd
-size 27218
+oid sha256:bbf6e55866620ad7422340380a4a02620840217f6b18cc184cfa4b3c2d15a63d
+size 27065
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.verifysession.impl_null_DefaultGroup_VerifySelfSessionViewLightPreview_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.verifysession.impl_null_DefaultGroup_VerifySelfSessionViewLightPreview_0_null_0,NEXUS_5,1.0,en].png
index b6a5f9ef16..f204672d04 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.verifysession.impl_null_DefaultGroup_VerifySelfSessionViewLightPreview_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.verifysession.impl_null_DefaultGroup_VerifySelfSessionViewLightPreview_0_null_0,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:a9d85fcaba465c6747e933974cf9712953222fe934eb2ef3acd513050c9ab19e
-size 29087
+oid sha256:4c3b9167b8b97eed74727902987a91fc1808d91709cef3a935b0fc58dbb4650a
+size 29049
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.verifysession.impl_null_DefaultGroup_VerifySelfSessionViewLightPreview_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.verifysession.impl_null_DefaultGroup_VerifySelfSessionViewLightPreview_0_null_1,NEXUS_5,1.0,en].png
index 7053e141af..0c9a371a45 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.verifysession.impl_null_DefaultGroup_VerifySelfSessionViewLightPreview_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.verifysession.impl_null_DefaultGroup_VerifySelfSessionViewLightPreview_0_null_1,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:ff5f22bda0c7deaaba6e8006abdd7e0c9ba1562016ae62ad41b6a70e67737fdc
-size 26179
+oid sha256:1f1c11cdf296cef8514547cee32fffc53eeb2ba16ab85475e98c66eaaa315f0f
+size 26294
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.verifysession.impl_null_DefaultGroup_VerifySelfSessionViewLightPreview_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.verifysession.impl_null_DefaultGroup_VerifySelfSessionViewLightPreview_0_null_2,NEXUS_5,1.0,en].png
index d1e830a2ce..262a0ccbf9 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.verifysession.impl_null_DefaultGroup_VerifySelfSessionViewLightPreview_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.verifysession.impl_null_DefaultGroup_VerifySelfSessionViewLightPreview_0_null_2,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:6faa634a10bf61fa3b84d396c8572586334a5554187fb9935c472d1a56e02884
-size 59497
+oid sha256:bb425ad24d840d6f85fcbc7f8629f58e2ba9b4f8173e1e728056d3185c2bdd49
+size 59691
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.verifysession.impl_null_DefaultGroup_VerifySelfSessionViewLightPreview_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.verifysession.impl_null_DefaultGroup_VerifySelfSessionViewLightPreview_0_null_3,NEXUS_5,1.0,en].png
index 1da6df2b42..9ef4777a07 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.verifysession.impl_null_DefaultGroup_VerifySelfSessionViewLightPreview_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.verifysession.impl_null_DefaultGroup_VerifySelfSessionViewLightPreview_0_null_3,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:649cd1ab79165a14c67000ce378fdba8abf3e30b10db306641f6f8d158194f0c
-size 59923
+oid sha256:0f5f76169e604b2f2a5ab5dc302b91bdc142fc2a0a17001f78d6d8f63774c3f0
+size 60130
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.verifysession.impl_null_DefaultGroup_VerifySelfSessionViewLightPreview_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.verifysession.impl_null_DefaultGroup_VerifySelfSessionViewLightPreview_0_null_4,NEXUS_5,1.0,en].png
index 526c311ff2..a5f8157309 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.verifysession.impl_null_DefaultGroup_VerifySelfSessionViewLightPreview_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.verifysession.impl_null_DefaultGroup_VerifySelfSessionViewLightPreview_0_null_4,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:1a6a7c9e187e4825524e292243dcb2e3c57bab49c750dac25016e279d639c56c
-size 32266
+oid sha256:8325684caab3d97fdc240748b85592a7fab64233055fbdb65e18e56786968e40
+size 32388
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.verifysession.impl_null_DefaultGroup_VerifySelfSessionViewLightPreview_0_null_5,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.verifysession.impl_null_DefaultGroup_VerifySelfSessionViewLightPreview_0_null_5,NEXUS_5,1.0,en].png
index 78effe0524..e8535fd92c 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.verifysession.impl_null_DefaultGroup_VerifySelfSessionViewLightPreview_0_null_5,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.verifysession.impl_null_DefaultGroup_VerifySelfSessionViewLightPreview_0_null_5,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:f9d94a1d66c85094c425f4054cc3115cab28e3109272657a96be939bbc169cf3
-size 26759
+oid sha256:78e9a0f4d9799f1a526e217398dd885d91bee6f69d4544d2f5799a170fefd02a
+size 26971
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.atoms_null_DefaultGroup_RoundedIconAtomDarkPreview_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.atoms_null_DefaultGroup_RoundedIconAtomDarkPreview_0_null,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..bb4c8a386d
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.atoms_null_DefaultGroup_RoundedIconAtomDarkPreview_0_null,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:fddc4b662ae2284c9179ccea787dbe4fe7cf4923bad81cdbaf891e8149b7131e
+size 8083
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.atoms_null_DefaultGroup_RoundedIconAtomLightPreview_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.atoms_null_DefaultGroup_RoundedIconAtomLightPreview_0_null,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..e4993027cc
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.atoms_null_DefaultGroup_RoundedIconAtomLightPreview_0_null,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:e46889dfdfcd8a9999992afcc75d73ea5dd8107a4f5fc19335f9022335c6fc7d
+size 6834
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.molecules_null_DefaultGroup_IconTitleSubtitleMoleculeDarkPreview_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.molecules_null_DefaultGroup_IconTitleSubtitleMoleculeDarkPreview_0_null,NEXUS_5,1.0,en].png
index 9f2b08ab8d..97f45a7e34 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.molecules_null_DefaultGroup_IconTitleSubtitleMoleculeDarkPreview_0_null,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.molecules_null_DefaultGroup_IconTitleSubtitleMoleculeDarkPreview_0_null,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:73d4db2c6cdd12c05672affc584c75161aa481fd0065c1b084f32ed4c3481c66
-size 10605
+oid sha256:d71452355496c1fe8214f061f72baf3c7690953138777952cdb65befdc133e57
+size 10407
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.molecules_null_DefaultGroup_IconTitleSubtitleMoleculeLightPreview_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.molecules_null_DefaultGroup_IconTitleSubtitleMoleculeLightPreview_0_null,NEXUS_5,1.0,en].png
index 010dd2b941..70d1d4e355 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.molecules_null_DefaultGroup_IconTitleSubtitleMoleculeLightPreview_0_null,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.molecules_null_DefaultGroup_IconTitleSubtitleMoleculeLightPreview_0_null,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:7aadaccb2874346a08bbdcf2cd8d51fd1490319e10c22065611c70543ce8d28d
-size 9695
+oid sha256:b508532be8dd29f60e30c00923834ac21ff0664cf3521fb86cdcdd493e9bfff7
+size 9930
diff --git a/tools/check/forbidden_strings_in_code.txt b/tools/check/forbidden_strings_in_code.txt
index ae346c170d..41a6c16466 100755
--- a/tools/check/forbidden_strings_in_code.txt
+++ b/tools/check/forbidden_strings_in_code.txt
@@ -129,3 +129,6 @@ System\.currentTimeMillis\(\)===1
### Suspicious String template. Please check that the string template will behave as expected, i.e. the class field and not the whole object will be used. For instance `Timber.d("$event.type")` is not correct, you should write `Timber.d("${event.type}")`. In the former the whole event content will be logged, since it's a data class. If this is expected (i.e. to fix false positive), please add explicit curly braces (`{` and `}`) around the variable, for instance `"elementLogs.${i}.txt"`
\$[a-zA-Z_]\w*\??\.[a-zA-Z_]
+
+### Use `import io.element.android.libraries.ui.strings.R as StringsR` then `StringR.string.` instead
+io\.element\.android\.libraries\.ui\.strings\.R\.
diff --git a/tools/localazy/config.json b/tools/localazy/config.json
index a23d652cd8..a7317af0db 100644
--- a/tools/localazy/config.json
+++ b/tools/localazy/config.json
@@ -73,7 +73,10 @@
"name": ":features:login:impl",
"includeRegex": [
"screen_login_.*",
- "screen_change_server_.*"
+ "screen_server_confirmation_.*",
+ "screen_change_server_.*",
+ "screen_change_account_provider_.*",
+ "screen_account_provider_.*"
]
},
{