Skip to content

Commit f73c0e4

Browse files
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>
1 parent 2ce1b17 commit f73c0e4

File tree

145 files changed

+1649
-817
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

145 files changed

+1649
-817
lines changed

appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt

+18-4
Original file line numberDiff line numberDiff line change
@@ -73,17 +73,21 @@ import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
7373
import io.element.android.libraries.matrix.api.core.UserId
7474
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
7575
import io.element.android.libraries.matrix.api.permalink.PermalinkData
76-
import io.element.android.libraries.matrix.api.verification.SessionVerificationRequestDetails
7776
import io.element.android.libraries.matrix.api.verification.SessionVerificationServiceListener
77+
import io.element.android.libraries.matrix.api.verification.VerificationRequest
7878
import io.element.android.services.appnavstate.api.AppNavigationStateService
7979
import kotlinx.coroutines.CoroutineScope
80+
import kotlinx.coroutines.MainScope
81+
import kotlinx.coroutines.delay
82+
import kotlinx.coroutines.flow.first
8083
import kotlinx.coroutines.flow.launchIn
8184
import kotlinx.coroutines.flow.onEach
8285
import kotlinx.coroutines.launch
8386
import kotlinx.parcelize.Parcelize
8487
import timber.log.Timber
8588
import java.util.Optional
8689
import java.util.UUID
90+
import kotlin.time.Duration.Companion.milliseconds
8791

8892
@ContributesNode(SessionScope::class)
8993
class LoggedInFlowNode @AssistedInject constructor(
@@ -127,8 +131,18 @@ class LoggedInFlowNode @AssistedInject constructor(
127131
)
128132

129133
private val verificationListener = object : SessionVerificationServiceListener {
130-
override fun onIncomingSessionRequest(sessionVerificationRequestDetails: SessionVerificationRequestDetails) {
131-
backstack.singleTop(NavTarget.IncomingVerificationRequest(sessionVerificationRequestDetails))
134+
override fun onIncomingSessionRequest(verificationRequest: VerificationRequest.Incoming) {
135+
// Without this launch the rendering and actual state of this Appyx node's children gets out of sync, resulting in a crash.
136+
// This might be because this method is called back from Rust in a background thread.
137+
MainScope().launch {
138+
// Wait until the app is in foreground to display the incoming verification request
139+
appNavigationStateService.appNavigationState.first { it.isInForeground }
140+
141+
// Wait for the UI to be ready
142+
delay(500.milliseconds)
143+
144+
backstack.singleTop(NavTarget.IncomingVerificationRequest(verificationRequest))
145+
}
132146
}
133147
}
134148

@@ -218,7 +232,7 @@ class LoggedInFlowNode @AssistedInject constructor(
218232
data object LogoutForNativeSlidingSyncMigrationNeeded : NavTarget
219233

220234
@Parcelize
221-
data class IncomingVerificationRequest(val data: SessionVerificationRequestDetails) : NavTarget
235+
data class IncomingVerificationRequest(val data: VerificationRequest.Incoming) : NavTarget
222236
}
223237

224238
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {

features/ftue/impl/build.gradle.kts

+12
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import extension.setupAnvil
2+
import org.gradle.kotlin.dsl.test
23

34
/*
45
* Copyright 2023, 2024 New Vector Ltd.
@@ -14,6 +15,12 @@ plugins {
1415

1516
android {
1617
namespace = "io.element.android.features.ftue.impl"
18+
19+
testOptions {
20+
unitTests {
21+
isIncludeAndroidResources = true
22+
}
23+
}
1724
}
1825

1926
setupAnvil()
@@ -30,19 +37,24 @@ dependencies {
3037
implementation(projects.libraries.uiStrings)
3138
implementation(projects.libraries.testtags)
3239
implementation(projects.features.analytics.api)
40+
implementation(projects.features.logout.api)
3341
implementation(projects.features.securebackup.api)
3442
implementation(projects.features.verifysession.api)
3543
implementation(projects.services.analytics.api)
3644
implementation(projects.features.lockscreen.api)
3745
implementation(projects.libraries.permissions.api)
3846
implementation(projects.libraries.permissions.noop)
3947
implementation(projects.services.toolbox.api)
48+
implementation(projects.appconfig)
4049

4150
testImplementation(libs.test.junit)
4251
testImplementation(libs.coroutines.test)
4352
testImplementation(libs.molecule.runtime)
4453
testImplementation(libs.test.truth)
4554
testImplementation(libs.test.turbine)
55+
testImplementation(libs.test.robolectric)
56+
testImplementation(libs.androidx.compose.ui.test.junit)
57+
testReleaseImplementation(libs.androidx.compose.ui.test.manifest)
4658
testImplementation(projects.libraries.matrix.test)
4759
testImplementation(projects.services.analytics.test)
4860
testImplementation(projects.services.analytics.noop)

features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/FtueFlowNode.kt

+1-13
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ import androidx.compose.ui.Modifier
1616
import androidx.lifecycle.lifecycleScope
1717
import com.bumble.appyx.core.lifecycle.subscribe
1818
import com.bumble.appyx.core.modality.BuildContext
19-
import com.bumble.appyx.core.navigation.backpresshandlerstrategies.BaseBackPressHandlerStrategy
2019
import com.bumble.appyx.core.node.Node
2120
import com.bumble.appyx.core.plugin.Plugin
2221
import com.bumble.appyx.navmodel.backstack.BackStack
@@ -38,8 +37,6 @@ import io.element.android.libraries.designsystem.theme.components.CircularProgre
3837
import io.element.android.libraries.di.AppScope
3938
import io.element.android.libraries.di.SessionScope
4039
import io.element.android.services.analytics.api.AnalyticsService
41-
import kotlinx.coroutines.flow.MutableStateFlow
42-
import kotlinx.coroutines.flow.StateFlow
4340
import kotlinx.coroutines.flow.distinctUntilChanged
4441
import kotlinx.coroutines.flow.filter
4542
import kotlinx.coroutines.flow.launchIn
@@ -59,7 +56,6 @@ class FtueFlowNode @AssistedInject constructor(
5956
backstack = BackStack(
6057
initialElement = NavTarget.Placeholder,
6158
savedStateMap = buildContext.savedStateMap,
62-
backPressHandler = NoOpBackstackHandlerStrategy(),
6359
),
6460
buildContext = buildContext,
6561
plugins = plugins,
@@ -104,7 +100,7 @@ class FtueFlowNode @AssistedInject constructor(
104100
NavTarget.Placeholder -> {
105101
createNode<PlaceholderNode>(buildContext)
106102
}
107-
NavTarget.SessionVerification -> {
103+
is NavTarget.SessionVerification -> {
108104
val callback = object : FtueSessionVerificationFlowNode.Callback {
109105
override fun onDone() {
110106
moveToNextStepIfNeeded()
@@ -175,11 +171,3 @@ class FtueFlowNode @AssistedInject constructor(
175171
}
176172
}
177173
}
178-
179-
private class NoOpBackstackHandlerStrategy<NavTarget : Any> : BaseBackPressHandlerStrategy<NavTarget, BackStack.State>() {
180-
override val canHandleBackPressFlow: StateFlow<Boolean> = MutableStateFlow(true)
181-
182-
override fun onBackPressed() {
183-
// No-op
184-
}
185-
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/*
2+
* Copyright 2025 New Vector Ltd.
3+
*
4+
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
5+
* Please see LICENSE files in the repository root for full details.
6+
*/
7+
8+
package io.element.android.features.ftue.impl.di
9+
10+
import com.squareup.anvil.annotations.ContributesTo
11+
import dagger.Binds
12+
import dagger.Module
13+
import io.element.android.features.ftue.impl.sessionverification.choosemode.ChooseSelfVerificationModePresenter
14+
import io.element.android.features.ftue.impl.sessionverification.choosemode.ChooseSelfVerificationModeState
15+
import io.element.android.libraries.architecture.Presenter
16+
import io.element.android.libraries.di.SessionScope
17+
18+
@ContributesTo(SessionScope::class)
19+
@Module
20+
interface FtueModule {
21+
@Binds
22+
fun bindChooseSelfVerificationMethodPresenter(presenter: ChooseSelfVerificationModePresenter): Presenter<ChooseSelfVerificationModeState>
23+
}

features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/FtueSessionVerificationFlowNode.kt

+48-10
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ package io.element.android.features.ftue.impl.sessionverification
99

1010
import android.os.Parcelable
1111
import androidx.compose.runtime.Composable
12+
import androidx.compose.runtime.mutableStateOf
1213
import androidx.compose.ui.Modifier
1314
import androidx.lifecycle.lifecycleScope
1415
import com.bumble.appyx.core.modality.BuildContext
@@ -17,15 +18,21 @@ import com.bumble.appyx.core.plugin.Plugin
1718
import com.bumble.appyx.core.plugin.plugins
1819
import com.bumble.appyx.navmodel.backstack.BackStack
1920
import com.bumble.appyx.navmodel.backstack.operation.newRoot
21+
import com.bumble.appyx.navmodel.backstack.operation.pop
2022
import com.bumble.appyx.navmodel.backstack.operation.push
2123
import dagger.assisted.Assisted
2224
import dagger.assisted.AssistedInject
2325
import io.element.android.anvilannotations.ContributesNode
26+
import io.element.android.appconfig.LearnMoreConfig
27+
import io.element.android.features.ftue.impl.sessionverification.choosemode.ChooseSelfVerificationModeNode
2428
import io.element.android.features.securebackup.api.SecureBackupEntryPoint
2529
import io.element.android.features.verifysession.api.VerifySessionEntryPoint
2630
import io.element.android.libraries.architecture.BackstackView
2731
import io.element.android.libraries.architecture.BaseFlowNode
32+
import io.element.android.libraries.architecture.createNode
33+
import io.element.android.libraries.designsystem.utils.OpenUrlInTabView
2834
import io.element.android.libraries.di.SessionScope
35+
import io.element.android.libraries.matrix.api.verification.VerificationRequest
2936
import kotlinx.coroutines.launch
3037
import kotlinx.parcelize.Parcelize
3138

@@ -37,15 +44,18 @@ class FtueSessionVerificationFlowNode @AssistedInject constructor(
3744
private val secureBackupEntryPoint: SecureBackupEntryPoint,
3845
) : BaseFlowNode<FtueSessionVerificationFlowNode.NavTarget>(
3946
backstack = BackStack(
40-
initialElement = NavTarget.Root(showDeviceVerifiedScreen = false),
47+
initialElement = NavTarget.Root,
4148
savedStateMap = buildContext.savedStateMap,
4249
),
4350
buildContext = buildContext,
4451
plugins = plugins,
4552
) {
4653
sealed interface NavTarget : Parcelable {
4754
@Parcelize
48-
data class Root(val showDeviceVerifiedScreen: Boolean) : NavTarget
55+
data object Root : NavTarget
56+
57+
@Parcelize
58+
data object UseAnotherDevice : NavTarget
4959

5060
@Parcelize
5161
data object EnterRecoveryKey : NavTarget
@@ -62,27 +72,51 @@ class FtueSessionVerificationFlowNode @AssistedInject constructor(
6272
override fun onDone() {
6373
lifecycleScope.launch {
6474
// Move to the completed state view in the verification flow
65-
backstack.newRoot(NavTarget.Root(showDeviceVerifiedScreen = true))
75+
backstack.newRoot(NavTarget.UseAnotherDevice)
6676
}
6777
}
6878
}
6979

7080
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
7181
return when (navTarget) {
7282
is NavTarget.Root -> {
83+
val callback = object : ChooseSelfVerificationModeNode.Callback {
84+
override fun onUseAnotherDevice() {
85+
backstack.push(NavTarget.UseAnotherDevice)
86+
}
87+
88+
override fun onUseRecoveryKey() {
89+
backstack.push(NavTarget.EnterRecoveryKey)
90+
}
91+
92+
override fun onResetKey() {
93+
backstack.push(NavTarget.ResetIdentity)
94+
}
95+
96+
override fun onLearnMoreAboutEncryption() {
97+
learnMoreUrl.value = LearnMoreConfig.ENCRYPTION_URL
98+
}
99+
}
100+
101+
createNode<ChooseSelfVerificationModeNode>(buildContext, plugins = listOf(callback))
102+
}
103+
is NavTarget.UseAnotherDevice -> {
73104
verifySessionEntryPoint.nodeBuilder(this, buildContext)
74-
.params(VerifySessionEntryPoint.Params(navTarget.showDeviceVerifiedScreen))
105+
.params(VerifySessionEntryPoint.Params(
106+
showDeviceVerifiedScreen = true,
107+
verificationRequest = VerificationRequest.Outgoing.CurrentSession,
108+
))
75109
.callback(object : VerifySessionEntryPoint.Callback {
76-
override fun onEnterRecoveryKey() {
77-
backstack.push(NavTarget.EnterRecoveryKey)
78-
}
79-
80110
override fun onDone() {
81111
plugins<Callback>().forEach { it.onDone() }
82112
}
83113

84-
override fun onResetKey() {
85-
backstack.push(NavTarget.ResetIdentity)
114+
override fun onBack() {
115+
backstack.pop()
116+
}
117+
118+
override fun onLearnMoreAboutEncryption() {
119+
learnMoreUrl.value = LearnMoreConfig.ENCRYPTION_URL
86120
}
87121
})
88122
.build()
@@ -106,8 +140,12 @@ class FtueSessionVerificationFlowNode @AssistedInject constructor(
106140
}
107141
}
108142

143+
private val learnMoreUrl = mutableStateOf<String?>(null)
144+
109145
@Composable
110146
override fun View(modifier: Modifier) {
111147
BackstackView()
148+
149+
OpenUrlInTabView(learnMoreUrl)
112150
}
113151
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/*
2+
* Copyright 2025 New Vector Ltd.
3+
*
4+
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
5+
* Please see LICENSE files in the repository root for full details.
6+
*/
7+
8+
package io.element.android.features.ftue.impl.sessionverification.choosemode
9+
10+
sealed interface ChooseSelfVerificationModeEvent {
11+
data object SignOut : ChooseSelfVerificationModeEvent
12+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/*
2+
* Copyright 2025 New Vector Ltd.
3+
*
4+
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
5+
* Please see LICENSE files in the repository root for full details.
6+
*/
7+
8+
package io.element.android.features.ftue.impl.sessionverification.choosemode
9+
10+
import androidx.compose.runtime.Composable
11+
import androidx.compose.ui.Modifier
12+
import com.bumble.appyx.core.modality.BuildContext
13+
import com.bumble.appyx.core.node.Node
14+
import com.bumble.appyx.core.plugin.Plugin
15+
import com.bumble.appyx.core.plugin.plugins
16+
import dagger.assisted.Assisted
17+
import dagger.assisted.AssistedInject
18+
import io.element.android.anvilannotations.ContributesNode
19+
import io.element.android.features.logout.api.direct.DirectLogoutView
20+
import io.element.android.libraries.architecture.Presenter
21+
import io.element.android.libraries.di.SessionScope
22+
23+
@ContributesNode(SessionScope::class)
24+
class ChooseSelfVerificationModeNode @AssistedInject constructor(
25+
@Assisted buildContext: BuildContext,
26+
@Assisted plugins: List<Plugin>,
27+
private val presenter: Presenter<ChooseSelfVerificationModeState>,
28+
private val directLogoutView: DirectLogoutView,
29+
) : Node(buildContext, plugins = plugins) {
30+
interface Callback : Plugin {
31+
fun onUseAnotherDevice()
32+
fun onUseRecoveryKey()
33+
fun onResetKey()
34+
fun onLearnMoreAboutEncryption()
35+
}
36+
37+
private val callback = plugins<Callback>().first()
38+
39+
@Composable
40+
override fun View(modifier: Modifier) {
41+
val state = presenter.present()
42+
43+
ChooseSelfVerificationModeView(
44+
state = state,
45+
onUseAnotherDevice = callback::onUseAnotherDevice,
46+
onUseRecoveryKey = callback::onUseRecoveryKey,
47+
onResetKey = callback::onResetKey,
48+
onLearnMore = callback::onLearnMoreAboutEncryption,
49+
modifier = modifier,
50+
)
51+
52+
directLogoutView.Render(state = state.directLogoutState)
53+
}
54+
}

0 commit comments

Comments
 (0)