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

@ -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()
}
}
}

View file

@ -75,6 +75,7 @@ class UserProfileNode @AssistedInject constructor(
onOpenDm = ::onStartDM,
onStartCall = callback::onStartCall,
openAvatarPreview = callback::openAvatarPreview,
onVerifyClick = callback::onVerifyUser,
)
}
}

View file

@ -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()