mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-10-31 08:15:02 +00:00
Ask for session verification before sharing with an unverified account
The incoming share is now displayed on top of the root, so the FTUE, and the session verification in particular, could be bypassed. - When the current account is not verified, wait for the FTUE to be complete before displaying the incoming share, as before. - When the user selects another account which is not verified in the room select screen, display the session verification flow of the FTUE for this account on top of the share. As in the FTUE, the user has to verify the session or to sign out. - Expose the FTUE session verification flow with a new SessionVerificationEntryPoint, displayed by OtherSessionVerificationNode which sets up the session graph of the account to verify. - Introduce RoomSelectNavigator so that RoomSelectPresenter can request the verification of the selected session. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -577,6 +577,15 @@ class LoggedInFlowNode(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for the home screen to be attached, i.e. for the FTUE to be complete.
|
||||
*/
|
||||
internal suspend fun waitForHome() {
|
||||
waitForNavTargetAttached { navTarget ->
|
||||
navTarget is NavTarget.Home
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun attachRoom(
|
||||
roomIdOrAlias: RoomIdOrAlias,
|
||||
serverNames: List<String> = emptyList(),
|
||||
|
||||
@@ -43,6 +43,7 @@ import io.element.android.appnav.session.MatrixSessionCache
|
||||
import io.element.android.appnav.verification.IncomingVerificationRequestData
|
||||
import io.element.android.appnav.verification.IncomingVerificationRequestObserver
|
||||
import io.element.android.appnav.verification.OtherSessionIncomingVerificationNode
|
||||
import io.element.android.appnav.verification.OtherSessionVerificationNode
|
||||
import io.element.android.features.announcement.api.AnnouncementService
|
||||
import io.element.android.features.login.api.LoginParams
|
||||
import io.element.android.features.login.api.accesscontrol.AccountProviderAccessControl
|
||||
@@ -244,6 +245,8 @@ class RootFlowNode(
|
||||
.withPreviousValue()
|
||||
.onEach { (previous, current) ->
|
||||
(previous.orEmpty() - current).forEach { sessionId ->
|
||||
// The verification of a removed session cannot continue, for instance if the user has signed out from it
|
||||
backstack.removeLast(NavTarget.OtherAccountSessionVerification(sessionId))
|
||||
Timber.d("Session $sessionId removed, remove it from the cache")
|
||||
matrixSessionCache.remove(sessionId)
|
||||
}
|
||||
@@ -391,6 +394,10 @@ class RootFlowNode(
|
||||
val sessionId: SessionId,
|
||||
val verificationRequest: VerificationRequest.Incoming,
|
||||
) : NavTarget
|
||||
|
||||
@Parcelize data class OtherAccountSessionVerification(
|
||||
val sessionId: SessionId,
|
||||
) : NavTarget
|
||||
}
|
||||
|
||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||
@@ -462,6 +469,20 @@ class RootFlowNode(
|
||||
}
|
||||
createNode<OtherSessionIncomingVerificationNode>(buildContext, plugins = listOf(inputs, callback))
|
||||
}
|
||||
is NavTarget.OtherAccountSessionVerification -> {
|
||||
if (matrixSessionCache.getOrNull(navTarget.sessionId) == null) {
|
||||
Timber.w("Couldn't find session ${navTarget.sessionId} to verify")
|
||||
lifecycleScope.launch { backstack.pop() }
|
||||
return emptyNode(buildContext)
|
||||
}
|
||||
val inputs = OtherSessionVerificationNode.Inputs(sessionId = navTarget.sessionId)
|
||||
val callback = object : OtherSessionVerificationNode.Callback {
|
||||
override fun onDone() {
|
||||
backstack.pop()
|
||||
}
|
||||
}
|
||||
createNode<OtherSessionVerificationNode>(buildContext, plugins = listOf(inputs, callback))
|
||||
}
|
||||
is NavTarget.IncomingShare -> {
|
||||
val callback = object : ShareEntryPoint.Callback {
|
||||
override fun onDone(sessionId: SessionId, roomIds: List<RoomId>) {
|
||||
@@ -480,6 +501,13 @@ class RootFlowNode(
|
||||
override fun onCancel() {
|
||||
backstack.pop()
|
||||
}
|
||||
|
||||
override fun onSessionVerificationRequired(sessionId: SessionId) {
|
||||
val navTarget = NavTarget.OtherAccountSessionVerification(sessionId)
|
||||
if (backstack.elements.value.lastOrNull()?.key?.navTarget != navTarget) {
|
||||
backstack.push(navTarget)
|
||||
}
|
||||
}
|
||||
}
|
||||
shareEntryPoint.createNode(
|
||||
parentNode = this,
|
||||
@@ -582,10 +610,11 @@ class RootFlowNode(
|
||||
// No session, open login
|
||||
switchToNotLoggedInFlow(null)
|
||||
} else {
|
||||
// Wait for the current session to be restored, then display the incoming share on top of it.
|
||||
// Wait for the current session to be restored and for the FTUE to be complete, so that the session is verified,
|
||||
// then display the incoming share on top of it.
|
||||
// The incoming share does not belong to the current session: in case of multiple accounts, the user can switch account
|
||||
// from the room select screen, and the share is kept if the current session is signed out in the meantime.
|
||||
attachSession(latestSessionId)
|
||||
attachSession(latestSessionId).waitForHome()
|
||||
backstack.push(NavTarget.IncomingShare(latestSessionId, shareIntentData))
|
||||
}
|
||||
}
|
||||
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
package io.element.android.appnav.verification
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.bumble.appyx.core.composable.Children
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.navigation.model.permanent.PermanentNavModel
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.node.ParentNode
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.annotations.ContributesNode
|
||||
import io.element.android.appnav.di.SessionGraphFactory
|
||||
import io.element.android.features.ftue.api.SessionVerificationEntryPoint
|
||||
import io.element.android.libraries.architecture.NodeInputs
|
||||
import io.element.android.libraries.architecture.callback
|
||||
import io.element.android.libraries.architecture.inputs
|
||||
import io.element.android.libraries.di.DependencyInjectionGraphOwner
|
||||
import io.element.android.libraries.matrix.api.MatrixClientProvider
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
/**
|
||||
* Display the verification of a session which is not the current one, without switching the current session.
|
||||
* It sets up the Session graph of the session to verify.
|
||||
*/
|
||||
@ContributesNode(AppScope::class)
|
||||
@AssistedInject
|
||||
class OtherSessionVerificationNode(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
sessionGraphFactory: SessionGraphFactory,
|
||||
matrixClientProvider: MatrixClientProvider,
|
||||
private val sessionVerificationEntryPoint: SessionVerificationEntryPoint,
|
||||
) : ParentNode<OtherSessionVerificationNode.NavTarget>(
|
||||
navModel = PermanentNavModel(
|
||||
navTargets = setOf(NavTarget),
|
||||
savedStateMap = buildContext.savedStateMap,
|
||||
),
|
||||
buildContext = buildContext,
|
||||
plugins = plugins
|
||||
), DependencyInjectionGraphOwner {
|
||||
interface Callback : Plugin {
|
||||
fun onDone()
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
object NavTarget : Parcelable
|
||||
|
||||
data class Inputs(
|
||||
val sessionId: SessionId,
|
||||
) : NodeInputs
|
||||
|
||||
private val callback: Callback = callback()
|
||||
private val inputs: Inputs = inputs()
|
||||
override val graph = sessionGraphFactory.create(
|
||||
requireNotNull(matrixClientProvider.getOrNull(inputs.sessionId)) { "No MatrixClient for session ${inputs.sessionId}" }
|
||||
)
|
||||
|
||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||
return sessionVerificationEntryPoint.createNode(
|
||||
parentNode = this,
|
||||
buildContext = buildContext,
|
||||
callback = object : SessionVerificationEntryPoint.Callback {
|
||||
override fun onDone() {
|
||||
callback.onDone()
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
Children(
|
||||
navModel = navModel,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user