Implement user verification (#4294)

* Add support for starting verification of a user

* Add support for replying to incoming user verification requests

* Add reset recovery key button and previews to `ChooseSelfVerificationModeView`

* Add 'Profile' item in room details screen

* Update screenshots

* Remove `showDeviceVerifiedScreen` parameter from `NavTarget.UseAnotherDevice`

* Allow exiting the FTUE flow, which will close the app. The previous state will be restored when the app is reopened.

* When outgoing verification fails, move to the `Canceled` state. Then, when resetting the state machine state also reset the verification service.

---------

Co-authored-by: ElementBot <android@element.io>
This commit is contained in:
Jorge Martin Espinosa 2025-03-10 11:20:17 +01:00 committed by GitHub
parent 2ce1b17dae
commit f73c0e42a4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
145 changed files with 1662 additions and 830 deletions

View file

@ -24,6 +24,7 @@ class UserProfileNodeHelper(
fun openAvatarPreview(username: String, avatarUrl: String)
fun onStartDM(roomId: RoomId)
fun onStartCall(dmRoomId: RoomId)
fun onVerifyUser(userId: UserId)
}
fun onShareUser(

View file

@ -38,6 +38,7 @@ import io.element.android.libraries.designsystem.theme.components.Scaffold
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.ui.components.CreateDmConfirmationBottomSheet
import io.element.android.libraries.ui.strings.CommonStrings
@ -50,6 +51,7 @@ fun UserProfileView(
onStartCall: (RoomId) -> Unit,
goBack: () -> Unit,
openAvatarPreview: (username: String, url: String) -> Unit,
onVerifyClick: (UserId) -> Unit,
modifier: Modifier = Modifier,
) {
Scaffold(
@ -82,7 +84,7 @@ fun UserProfileView(
)
Spacer(modifier = Modifier.height(26.dp))
if (!state.isCurrentUser) {
VerifyUserSection(state)
VerifyUserSection(state, onVerifyClick = { onVerifyClick(state.userId) })
BlockUserSection(state)
BlockUserDialogs(state)
}
@ -116,14 +118,15 @@ fun UserProfileView(
}
@Composable
private fun VerifyUserSection(state: UserProfileState) {
private fun VerifyUserSection(
state: UserProfileState,
onVerifyClick: () -> Unit,
) {
if (state.isVerified.dataOrNull() == false) {
ListItem(
headlineContent = { Text(stringResource(CommonStrings.common_verify_identity)) },
supportingContent = { Text(stringResource(R.string.screen_room_member_details_verify_button_subtitle)) },
headlineContent = { Text(stringResource(CommonStrings.common_verify_user)) },
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Lock())),
enabled = false,
onClick = { },
onClick = onVerifyClick,
)
}
}
@ -139,6 +142,7 @@ internal fun UserProfileViewPreview(
goBack = {},
onOpenDm = {},
onStartCall = {},
openAvatarPreview = { _, _ -> }
openAvatarPreview = { _, _ -> },
onVerifyClick = {},
)
}

View file

@ -20,8 +20,10 @@ import io.element.android.features.userprofile.shared.UserProfileView
import io.element.android.features.userprofile.shared.aUserProfileState
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.test.AN_AVATAR_URL
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_USER_ID
import io.element.android.libraries.matrix.test.A_USER_NAME
import io.element.android.libraries.testtags.TestTags
import io.element.android.libraries.ui.strings.CommonStrings
@ -193,6 +195,17 @@ class UserProfileViewTest {
rule.clickOn(CommonStrings.action_cancel)
eventsRecorder.assertSingle(UserProfileEvents.ClearConfirmationDialog)
}
@Test
fun `on verify user clicked - the right callback is called`() = runTest {
ensureCalledOnceWithParam(A_USER_ID) { callback ->
rule.setUserProfileView(
state = aUserProfileState(userId = A_USER_ID, isVerified = AsyncData.Success(false)),
onVerifyClick = callback,
)
rule.clickOn(CommonStrings.common_verify_user)
}
}
}
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setUserProfileView(
@ -202,6 +215,7 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setUserP
onShareUser: () -> Unit = EnsureNeverCalled(),
onDmStarted: (RoomId) -> Unit = EnsureNeverCalledWithParam(),
onStartCall: (RoomId) -> Unit = EnsureNeverCalledWithParam(),
onVerifyClick: (UserId) -> Unit = EnsureNeverCalledWithParam(),
goBack: () -> Unit = EnsureNeverCalled(),
openAvatarPreview: (String, String) -> Unit = EnsureNeverCalledWithTwoParams(),
) {
@ -213,6 +227,7 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setUserP
onStartCall = onStartCall,
goBack = goBack,
openAvatarPreview = openAvatarPreview,
onVerifyClick = onVerifyClick,
)
}
}