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:
parent
6f54de58f3
commit
b91933625c
145 changed files with 1662 additions and 830 deletions
|
|
@ -33,6 +33,7 @@ dependencies {
|
|||
implementation(projects.libraries.androidutils)
|
||||
implementation(projects.libraries.mediaviewer.api)
|
||||
implementation(projects.features.call.api)
|
||||
implementation(projects.features.verifysession.api)
|
||||
api(projects.features.userprofile.api)
|
||||
api(projects.features.userprofile.shared)
|
||||
implementation(libs.coil.compose)
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import io.element.android.features.call.api.ElementCallEntryPoint
|
|||
import io.element.android.features.userprofile.api.UserProfileEntryPoint
|
||||
import io.element.android.features.userprofile.impl.root.UserProfileNode
|
||||
import io.element.android.features.userprofile.shared.UserProfileNodeHelper
|
||||
import io.element.android.features.verifysession.api.VerifySessionEntryPoint
|
||||
import io.element.android.libraries.architecture.BackstackView
|
||||
import io.element.android.libraries.architecture.BaseFlowNode
|
||||
import io.element.android.libraries.architecture.createNode
|
||||
|
|
@ -32,7 +33,9 @@ import io.element.android.libraries.architecture.inputs
|
|||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.user.CurrentSessionIdHolder
|
||||
import io.element.android.libraries.matrix.api.verification.VerificationRequest
|
||||
import io.element.android.libraries.mediaviewer.api.MediaViewerEntryPoint
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
|
|
@ -43,6 +46,7 @@ class UserProfileFlowNode @AssistedInject constructor(
|
|||
private val elementCallEntryPoint: ElementCallEntryPoint,
|
||||
private val sessionIdHolder: CurrentSessionIdHolder,
|
||||
private val mediaViewerEntryPoint: MediaViewerEntryPoint,
|
||||
private val verifySessionEntryPoint: VerifySessionEntryPoint,
|
||||
) : BaseFlowNode<UserProfileFlowNode.NavTarget>(
|
||||
backstack = BackStack(
|
||||
initialElement = NavTarget.Root,
|
||||
|
|
@ -57,6 +61,9 @@ class UserProfileFlowNode @AssistedInject constructor(
|
|||
|
||||
@Parcelize
|
||||
data class AvatarPreview(val name: String, val avatarUrl: String) : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data class VerifyUser(val userId: UserId) : NavTarget
|
||||
}
|
||||
|
||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||
|
|
@ -74,6 +81,10 @@ class UserProfileFlowNode @AssistedInject constructor(
|
|||
override fun onStartCall(dmRoomId: RoomId) {
|
||||
elementCallEntryPoint.startCall(CallType.RoomCall(sessionId = sessionIdHolder.current, roomId = dmRoomId))
|
||||
}
|
||||
|
||||
override fun onVerifyUser(userId: UserId) {
|
||||
backstack.push(NavTarget.VerifyUser(userId))
|
||||
}
|
||||
}
|
||||
val params = UserProfileNode.UserProfileInputs(userId = inputs<UserProfileEntryPoint.Params>().userId)
|
||||
createNode<UserProfileNode>(buildContext, listOf(callback, params))
|
||||
|
|
@ -96,6 +107,15 @@ class UserProfileFlowNode @AssistedInject constructor(
|
|||
.callback(callback)
|
||||
.build()
|
||||
}
|
||||
is NavTarget.VerifyUser -> {
|
||||
val params = VerifySessionEntryPoint.Params(
|
||||
showDeviceVerifiedScreen = false,
|
||||
verificationRequest = VerificationRequest.Outgoing.User(userId = navTarget.userId)
|
||||
)
|
||||
verifySessionEntryPoint.nodeBuilder(this, buildContext)
|
||||
.params(params)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -75,6 +75,7 @@ class UserProfileNode @AssistedInject constructor(
|
|||
onOpenDm = ::onStartDM,
|
||||
onStartCall = callback::onStartCall,
|
||||
openAvatarPreview = callback::openAvatarPreview,
|
||||
onVerifyClick = callback::onVerifyUser,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -73,7 +73,6 @@ class UserProfilePresenter @AssistedInject constructor(
|
|||
val coroutineScope = rememberCoroutineScope()
|
||||
val isCurrentUser = remember { client.isMe(userId) }
|
||||
var confirmationDialog by remember { mutableStateOf<ConfirmationDialog?>(null) }
|
||||
var userProfile by remember { mutableStateOf<MatrixUser?>(null) }
|
||||
val startDmActionState: MutableState<AsyncAction<RoomId>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
|
||||
val isBlocked: MutableState<AsyncData<Boolean>> = remember { mutableStateOf(AsyncData.Uninitialized) }
|
||||
val isVerified: MutableState<AsyncData<Boolean>> = remember { mutableStateOf(AsyncData.Uninitialized) }
|
||||
|
|
@ -86,9 +85,8 @@ class UserProfilePresenter @AssistedInject constructor(
|
|||
.onEach { isBlocked.value = AsyncData.Success(it) }
|
||||
.launchIn(this)
|
||||
}
|
||||
LaunchedEffect(Unit) {
|
||||
userProfile = client.getProfile(userId).getOrNull()
|
||||
}
|
||||
val userProfile by produceState<MatrixUser?>(null) { value = client.getProfile(userId).getOrNull() }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
suspend {
|
||||
client.encryptionService().isUserVerified(userId).getOrThrow()
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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 = {},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue