Merge pull request #7918 from element-hq/feature/bma/iterateOnAddAccountUi

Add a back button to the server screen and iterate on the identity confirmation screen
This commit is contained in:
Benoit Marty
2026-10-14 17:16:35 +02:00
committed by GitHub
31 changed files with 140 additions and 46 deletions
@@ -46,11 +46,12 @@ class ChooseSelfVerificationModeStatePreviewParam :
fun aChooseSelfVerificationModeState(
buttonsState: AsyncData<ChooseSelfVerificationModeState.ButtonsState> = AsyncData.Success(aButtonsState()),
currentUser: MatrixUser = aMatrixUser(),
eventSink: (ChooseSelfVerificationModeEvent) -> Unit = {},
) = ChooseSelfVerificationModeState(
currentUser = currentUser,
buttonsState = buttonsState,
directLogoutState = aDirectLogoutState(),
eventSink = {},
eventSink = eventSink,
)
fun aButtonsState(
@@ -36,6 +36,7 @@ import io.element.android.libraries.designsystem.components.BigIcon
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Button
import io.element.android.libraries.designsystem.theme.components.IconSource
import io.element.android.libraries.designsystem.theme.components.OutlinedButton
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.components.TextButton
@@ -45,6 +46,9 @@ import io.element.android.libraries.matrix.ui.components.accountinfo.AccountInfo
import io.element.android.libraries.matrix.ui.components.accountinfo.AccountInfoCardState
import io.element.android.libraries.ui.strings.CommonStrings
/**
* Ref: https://www.figma.com/design/kEAcfun9iSpszeUDvdKZ6b/ER-351--Multi-account-in-EX?node-id=432-31813
*/
@Composable
fun ChooseSelfVerificationModeView(
state: ChooseSelfVerificationModeState,
@@ -65,7 +69,7 @@ fun ChooseSelfVerificationModeView(
title = {},
actions = {
TextButton(
text = stringResource(CommonStrings.action_signout),
text = stringResource(R.string.screen_identity_confirmation_sign_out),
onClick = { state.eventSink(ChooseSelfVerificationModeEvent.SignOut) }
)
}
@@ -141,6 +145,7 @@ private fun ChooseSelfVerificationModeButtons(
modifier = Modifier.fillMaxWidth(),
text = stringResource(R.string.screen_identity_use_another_device),
onClick = onUseAnotherDevice,
leadingIcon = IconSource.Vector(CompoundIcons.Devices()),
)
}
if (state.buttonsState.data.canUseRecoveryKey) {
@@ -2,6 +2,7 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_identity_confirmation_cannot_confirm">"Can\'t confirm?"</string>
<string name="screen_identity_confirmation_create_new_recovery_key">"Create a new recovery key"</string>
<string name="screen_identity_confirmation_sign_out">"Sign out"</string>
<string name="screen_identity_confirmation_subtitle">"Choose how to verify to set up secure messaging."</string>
<string name="screen_identity_confirmation_title">"Confirm your digital identity"</string>
<string name="screen_identity_confirmation_use_another_device">"Use another device"</string>
@@ -18,6 +18,7 @@ import io.element.android.features.ftue.impl.R
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.tests.testutils.EnsureNeverCalled
import io.element.android.tests.testutils.EventsRecorder
import io.element.android.tests.testutils.clickOn
import io.element.android.tests.testutils.ensureCalledOnce
import io.element.android.tests.testutils.robolectric.RobolectricTest
@@ -73,6 +74,16 @@ class ChooseSessionVerificationModeViewTest : RobolectricTest() {
}
}
@Test
fun `clicking on sign out emits the expected event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<ChooseSelfVerificationModeEvent>()
setChooseSelfVerificationModeView(
aChooseSelfVerificationModeState(eventSink = eventsRecorder),
)
clickOn(R.string.screen_identity_confirmation_sign_out)
eventsRecorder.assertSingle(ChooseSelfVerificationModeEvent.SignOut)
}
private fun AndroidComposeUiTest<ComponentActivity>.setChooseSelfVerificationModeView(
state: ChooseSelfVerificationModeState,
onLearnMoreClick: () -> Unit = EnsureNeverCalled(),
@@ -56,6 +56,7 @@ class ConfirmAccountProviderNode(
ConfirmAccountProviderView(
state = state,
modifier = modifier,
onBackClick = ::navigateUp,
onOAuthDetails = callback::navigateToOAuth,
onNeedLoginPassword = callback::navigateToLoginPassword,
onLearnMoreClick = { openLearnMorePage(context) },
@@ -34,7 +34,7 @@ open class ConfirmAccountProviderStatePreviewParam : PreviewParameterProvider<Co
)
}
private fun aConfirmAccountProviderState(
internal fun aConfirmAccountProviderState(
accountProviderInput: String = "matrix.org",
accountProviderSuggestion: String? = null,
isAccountCreation: Boolean = false,
@@ -14,6 +14,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@@ -50,12 +51,14 @@ import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMo
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.BigIcon
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.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Button
import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.TextField
import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.matrix.api.auth.OAuthDetails
import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionDialogView
import io.element.android.libraries.testtags.TestTags
@@ -71,6 +74,7 @@ import io.element.android.libraries.ui.strings.CommonStrings
@Composable
fun ConfirmAccountProviderView(
state: ConfirmAccountProviderState,
onBackClick: () -> Unit,
onOAuthDetails: (OAuthDetails) -> Unit,
onNeedLoginPassword: () -> Unit,
onLearnMoreClick: () -> Unit,
@@ -118,9 +122,15 @@ fun ConfirmAccountProviderView(
// Let the header and content scroll so the supporting text is not clipped by the footer when the
// keyboard is visible and vertical space is reduced.
isScrollable = true,
topBar = {
TopAppBar(
title = {},
navigationIcon = { BackButton(onClick = onBackClick) },
colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent),
)
},
header = {
IconTitleSubtitleMolecule(
modifier = Modifier.padding(top = 60.dp),
iconStyle = BigIcon.Style.Default(CompoundIcons.UserProfileSolid()),
title = stringResource(
id = if (state.isAccountCreation) {
@@ -272,6 +282,7 @@ internal fun ConfirmAccountProviderViewPreview(
) = ElementPreview {
ConfirmAccountProviderView(
state = state,
onBackClick = {},
onOAuthDetails = {},
onNeedLoginPassword = {},
onLearnMoreClick = {},
@@ -180,6 +180,9 @@ private fun AddFirstAccountScaffold(
)
}
/**
* Ref: https://www.figma.com/design/kEAcfun9iSpszeUDvdKZ6b/ER-351--Multi-account-in-EX?node-id=147-34607
*/
@Composable
private fun AddOtherAccountScaffold(
loginView: @Composable () -> Unit,
@@ -189,8 +192,9 @@ private fun AddOtherAccountScaffold(
) {
FlowStepPage(
modifier = modifier,
title = stringResource(CommonStrings.common_add_account),
iconStyle = BigIcon.Style.Default(CompoundIcons.HomeSolid()),
title = stringResource(R.string.screen_onboarding_add_account_title),
subTitle = stringResource(R.string.screen_onboarding_add_account_subtitle),
iconStyle = BigIcon.Style.Default(CompoundIcons.Plus()),
buttons = { buttons() },
content = loginView,
onBackClick = onBackClick,
@@ -52,6 +52,8 @@
<string name="screen_missing_key_backup_step_4">"Follow the instructions to enable your key storage"</string>
<string name="screen_missing_key_backup_step_5">"Come back to %1$s"</string>
<string name="screen_missing_key_backup_title">"Enable your key storage before proceeding to %1$s"</string>
<string name="screen_onboarding_add_account_subtitle">"You can add multiple accounts to the app and switch between them easily."</string>
<string name="screen_onboarding_add_account_title">"Sign in to another account"</string>
<string name="screen_onboarding_app_version">"Version %1$s"</string>
<string name="screen_onboarding_checking_account">"Checking account"</string>
<string name="screen_onboarding_sign_in_manually">"Sign in manually"</string>
@@ -0,0 +1,57 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@file:OptIn(ExperimentalTestApi::class)
package io.element.android.features.login.impl.screens.confirmaccountprovider
import androidx.activity.ComponentActivity
import androidx.compose.ui.test.AndroidComposeUiTest
import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
import io.element.android.libraries.matrix.api.auth.OAuthDetails
import io.element.android.tests.testutils.EnsureNeverCalled
import io.element.android.tests.testutils.EnsureNeverCalledWithParam
import io.element.android.tests.testutils.EventsRecorder
import io.element.android.tests.testutils.ensureCalledOnce
import io.element.android.tests.testutils.pressBack
import io.element.android.tests.testutils.robolectric.RobolectricTest
import org.junit.Test
class ConfirmAccountProviderViewTest : RobolectricTest() {
@Test
fun `clicking on back invokes the expected callback`() = runAndroidComposeUiTest {
val eventSink = EventsRecorder<ConfirmAccountProviderEvent>(expectEvents = false)
ensureCalledOnce {
setConfirmAccountProviderView(
state = aConfirmAccountProviderState(
eventSink = eventSink,
),
onBackClick = it,
)
pressBack()
}
}
private fun AndroidComposeUiTest<ComponentActivity>.setConfirmAccountProviderView(
state: ConfirmAccountProviderState,
onBackClick: () -> Unit = EnsureNeverCalled(),
onOAuthDetails: (OAuthDetails) -> Unit = EnsureNeverCalledWithParam(),
onNeedLoginPassword: () -> Unit = EnsureNeverCalled(),
onLearnMoreClick: () -> Unit = EnsureNeverCalled(),
) {
setContent {
ConfirmAccountProviderView(
state = state,
onBackClick = onBackClick,
onOAuthDetails = onOAuthDetails,
onNeedLoginPassword = onNeedLoginPassword,
onLearnMoreClick = onLearnMoreClick,
)
}
}
}
@@ -2,6 +2,7 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_identity_confirmation_cannot_confirm">"Can\'t confirm?"</string>
<string name="screen_identity_confirmation_create_new_recovery_key">"Create a new recovery key"</string>
<string name="screen_identity_confirmation_sign_out">"Sign out"</string>
<string name="screen_identity_confirmation_subtitle">"Choose how to verify to set up secure messaging."</string>
<string name="screen_identity_confirmation_title">"Confirm your digital identity"</string>
<string name="screen_identity_confirmation_use_another_device">"Use another device"</string>