Skip to content

Commit 1045f99

Browse files
jmartinespElementBot
andauthored
Add SessionData.needsVerification field (#2672)
* Add `SessionData.needsVerification`: - Allows us to add a skip button for debug builds. - We can have the verification state almost instantly. - It doesn't depend on network availability to know the verification state and display the UI. * Add DB migration. - Make the skip button in the verification flow skip the whole flow including the completed screen. - Save the session as verified in `RustEncryptionService.recover(recoveryKey)`. * Enforce session verification for existing users too. * Fix verification confirmed screen subtitle (typo in id, was using the wrong string) * Update screenshots --------- Co-authored-by: ElementBot <[email protected]>
1 parent 63f7def commit 1045f99

File tree

44 files changed

+386
-123
lines changed

Some content is hidden

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

44 files changed

+386
-123
lines changed

.maestro/tests/account/verifySession.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,7 @@ appId: ${MAESTRO_APP_ID}
77
- inputText: ${MAESTRO_RECOVERY_KEY}
88
- hideKeyboard
99
- tapOn: "Confirm"
10+
- extendedWaitUntil:
11+
visible: "Device verified"
12+
timeout: 10000
13+
- tapOn: "Continue"

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,13 @@ package io.element.android.features.ftue.impl.sessionverification
1919
import android.os.Parcelable
2020
import androidx.compose.runtime.Composable
2121
import androidx.compose.ui.Modifier
22+
import androidx.lifecycle.lifecycleScope
2223
import com.bumble.appyx.core.modality.BuildContext
2324
import com.bumble.appyx.core.node.Node
2425
import com.bumble.appyx.core.plugin.Plugin
2526
import com.bumble.appyx.core.plugin.plugins
2627
import com.bumble.appyx.navmodel.backstack.BackStack
28+
import com.bumble.appyx.navmodel.backstack.operation.newRoot
2729
import com.bumble.appyx.navmodel.backstack.operation.push
2830
import dagger.assisted.Assisted
2931
import dagger.assisted.AssistedInject
@@ -33,6 +35,7 @@ import io.element.android.features.verifysession.api.VerifySessionEntryPoint
3335
import io.element.android.libraries.architecture.BackstackView
3436
import io.element.android.libraries.architecture.BaseFlowNode
3537
import io.element.android.libraries.di.SessionScope
38+
import kotlinx.coroutines.launch
3639
import kotlinx.parcelize.Parcelize
3740

3841
@ContributesNode(SessionScope::class)
@@ -83,7 +86,10 @@ class FtueSessionVerificationFlowNode @AssistedInject constructor(
8386
.params(SecureBackupEntryPoint.Params(SecureBackupEntryPoint.InitialTarget.EnterRecoveryKey))
8487
.callback(object : SecureBackupEntryPoint.Callback {
8588
override fun onDone() {
86-
callback.onDone()
89+
lifecycleScope.launch {
90+
// Move to the completed state view in the verification flow
91+
backstack.newRoot(NavTarget.Root)
92+
}
8793
}
8894
})
8995
.build()

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

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ import io.element.android.features.ftue.api.state.FtueState
2525
import io.element.android.features.lockscreen.api.LockScreenService
2626
import io.element.android.libraries.di.SessionScope
2727
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
28-
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
2928
import io.element.android.libraries.permissions.api.PermissionStateProvider
3029
import io.element.android.services.analytics.api.AnalyticsService
3130
import io.element.android.services.toolbox.api.sdk.BuildVersionSdkIntProvider
@@ -56,7 +55,7 @@ class DefaultFtueService @Inject constructor(
5655
}
5756

5857
init {
59-
sessionVerificationService.sessionVerifiedStatus
58+
sessionVerificationService.needsVerificationFlow
6059
.onEach { updateState() }
6160
.launchIn(coroutineScope)
6261

@@ -99,12 +98,8 @@ class DefaultFtueService @Inject constructor(
9998
).any { it() }
10099
}
101100

102-
private fun isSessionVerificationServiceReady(): Boolean {
103-
return sessionVerificationService.sessionVerifiedStatus.value != SessionVerifiedStatus.Unknown
104-
}
105-
106101
private fun isSessionNotVerified(): Boolean {
107-
return sessionVerificationService.sessionVerifiedStatus.value == SessionVerifiedStatus.NotVerified
102+
return sessionVerificationService.needsVerificationFlow.value
108103
}
109104

110105
private fun needsAnalyticsOptIn(): Boolean {
@@ -132,7 +127,6 @@ class DefaultFtueService @Inject constructor(
132127
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
133128
internal fun updateState() {
134129
state.value = when {
135-
!isSessionVerificationServiceReady() -> FtueState.Unknown
136130
isAnyStepIncomplete() -> FtueState.Incomplete
137131
else -> FtueState.Complete
138132
}

features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/DefaultFtueServiceTests.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ class DefaultFtueServiceTests {
9090
fun `traverse flow`() = runTest {
9191
val sessionVerificationService = FakeSessionVerificationService().apply {
9292
givenVerifiedStatus(SessionVerifiedStatus.NotVerified)
93+
givenNeedsVerification(true)
9394
}
9495
val analyticsService = FakeAnalyticsService()
9596
val permissionStateProvider = FakePermissionStateProvider(permissionGranted = false)
@@ -107,7 +108,7 @@ class DefaultFtueServiceTests {
107108

108109
// Session verification
109110
steps.add(state.getNextStep(steps.lastOrNull()))
110-
sessionVerificationService.givenVerifiedStatus(SessionVerifiedStatus.Verified)
111+
sessionVerificationService.givenNeedsVerification(false)
111112

112113
// Notifications opt in
113114
steps.add(state.getNextStep(steps.lastOrNull()))

features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutStateProvider.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ fun aSignedOutState() = SignedOutState(
3838
fun aSessionData(
3939
sessionId: SessionId = SessionId("@alice:server.org"),
4040
isTokenValid: Boolean = false,
41+
needsVerification: Boolean = false,
4142
): SessionData {
4243
return SessionData(
4344
userId = sessionId.value,
@@ -51,5 +52,6 @@ fun aSessionData(
5152
isTokenValid = isTokenValid,
5253
loginType = LoginType.UNKNOWN,
5354
passphrase = null,
55+
needsVerification = needsVerification,
5456
)
5557
}

features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionPresenter.kt

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,14 @@ import androidx.compose.runtime.LaunchedEffect
2323
import androidx.compose.runtime.collectAsState
2424
import androidx.compose.runtime.derivedStateOf
2525
import androidx.compose.runtime.getValue
26+
import androidx.compose.runtime.mutableStateOf
2627
import androidx.compose.runtime.remember
28+
import androidx.compose.runtime.rememberCoroutineScope
29+
import androidx.compose.runtime.setValue
2730
import com.freeletics.flowredux.compose.rememberStateAndDispatch
2831
import io.element.android.libraries.architecture.AsyncData
2932
import io.element.android.libraries.architecture.Presenter
33+
import io.element.android.libraries.core.meta.BuildMeta
3034
import io.element.android.libraries.matrix.api.encryption.EncryptionService
3135
import io.element.android.libraries.matrix.api.encryption.RecoveryState
3236
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
@@ -35,6 +39,7 @@ import kotlinx.coroutines.CoroutineScope
3539
import kotlinx.coroutines.ExperimentalCoroutinesApi
3640
import kotlinx.coroutines.flow.launchIn
3741
import kotlinx.coroutines.flow.onEach
42+
import kotlinx.coroutines.launch
3843
import javax.inject.Inject
3944
import io.element.android.features.verifysession.impl.VerifySelfSessionStateMachine.Event as StateMachineEvent
4045
import io.element.android.features.verifysession.impl.VerifySelfSessionStateMachine.State as StateMachineState
@@ -43,20 +48,28 @@ class VerifySelfSessionPresenter @Inject constructor(
4348
private val sessionVerificationService: SessionVerificationService,
4449
private val encryptionService: EncryptionService,
4550
private val stateMachine: VerifySelfSessionStateMachine,
51+
private val buildMeta: BuildMeta,
4652
) : Presenter<VerifySelfSessionState> {
4753
@Composable
4854
override fun present(): VerifySelfSessionState {
55+
val coroutineScope = rememberCoroutineScope()
4956
LaunchedEffect(Unit) {
5057
// Force reset, just in case the service was left in a broken state
5158
sessionVerificationService.reset()
5259
}
5360
val recoveryState by encryptionService.recoveryStateStateFlow.collectAsState()
5461
val stateAndDispatch = stateMachine.rememberStateAndDispatch()
62+
var skipVerification by remember { mutableStateOf(false) }
63+
val needsVerification by sessionVerificationService.needsVerificationFlow.collectAsState()
5564
val verificationFlowStep by remember {
5665
derivedStateOf {
57-
stateAndDispatch.state.value.toVerificationStep(
58-
canEnterRecoveryKey = recoveryState == RecoveryState.INCOMPLETE
59-
)
66+
when {
67+
skipVerification -> VerifySelfSessionState.VerificationStep.Skipped
68+
needsVerification -> stateAndDispatch.state.value.toVerificationStep(
69+
canEnterRecoveryKey = recoveryState == RecoveryState.INCOMPLETE
70+
)
71+
else -> VerifySelfSessionState.VerificationStep.Completed
72+
}
6073
}
6174
}
6275
// Start this after observing state machine
@@ -72,10 +85,15 @@ class VerifySelfSessionPresenter @Inject constructor(
7285
VerifySelfSessionViewEvents.DeclineVerification -> stateAndDispatch.dispatchAction(StateMachineEvent.DeclineChallenge)
7386
VerifySelfSessionViewEvents.Cancel -> stateAndDispatch.dispatchAction(StateMachineEvent.Cancel)
7487
VerifySelfSessionViewEvents.Reset -> stateAndDispatch.dispatchAction(StateMachineEvent.Reset)
88+
VerifySelfSessionViewEvents.SkipVerification -> coroutineScope.launch {
89+
sessionVerificationService.saveVerifiedState(true)
90+
skipVerification = true
91+
}
7592
}
7693
}
7794
return VerifySelfSessionState(
7895
verificationFlowStep = verificationFlowStep,
96+
displaySkipButton = buildMeta.isDebuggable,
7997
eventSink = ::handleEvents,
8098
)
8199
}

features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionState.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import io.element.android.libraries.matrix.api.verification.SessionVerificationD
2424
@Immutable
2525
data class VerifySelfSessionState(
2626
val verificationFlowStep: VerificationStep,
27+
val displaySkipButton: Boolean,
2728
val eventSink: (VerifySelfSessionViewEvents) -> Unit,
2829
) {
2930
@Stable
@@ -34,5 +35,6 @@ data class VerifySelfSessionState(
3435
data object Ready : VerificationStep
3536
data class Verifying(val data: SessionVerificationData, val state: AsyncData<Unit>) : VerificationStep
3637
data object Completed : VerificationStep
38+
data object Skipped : VerificationStep
3739
}
3840
}

features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionStateProvider.kt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import io.element.android.libraries.matrix.api.verification.VerificationEmoji
2424
open class VerifySelfSessionStateProvider : PreviewParameterProvider<VerifySelfSessionState> {
2525
override val values: Sequence<VerifySelfSessionState>
2626
get() = sequenceOf(
27-
aVerifySelfSessionState(),
27+
aVerifySelfSessionState(displaySkipButton = true),
2828
aVerifySelfSessionState(
2929
verificationFlowStep = VerifySelfSessionState.VerificationStep.AwaitingOtherDeviceResponse
3030
),
@@ -46,6 +46,10 @@ open class VerifySelfSessionStateProvider : PreviewParameterProvider<VerifySelfS
4646
aVerifySelfSessionState(
4747
verificationFlowStep = VerifySelfSessionState.VerificationStep.Initial(true)
4848
),
49+
aVerifySelfSessionState(
50+
verificationFlowStep = VerifySelfSessionState.VerificationStep.Completed,
51+
displaySkipButton = true,
52+
),
4953
// Add other state here
5054
)
5155
}
@@ -64,9 +68,11 @@ private fun aDecimalsSessionVerificationData(
6468

6569
internal fun aVerifySelfSessionState(
6670
verificationFlowStep: VerifySelfSessionState.VerificationStep = VerifySelfSessionState.VerificationStep.Initial(false),
71+
displaySkipButton: Boolean = false,
6772
eventSink: (VerifySelfSessionViewEvents) -> Unit = {},
6873
) = VerifySelfSessionState(
6974
verificationFlowStep = verificationFlowStep,
75+
displaySkipButton = displaySkipButton,
7076
eventSink = eventSink,
7177
)
7278

features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionView.kt

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,12 @@ import androidx.compose.foundation.layout.height
2828
import androidx.compose.foundation.layout.padding
2929
import androidx.compose.foundation.layout.size
3030
import androidx.compose.foundation.layout.widthIn
31+
import androidx.compose.material3.ExperimentalMaterial3Api
3132
import androidx.compose.material3.MaterialTheme
3233
import androidx.compose.runtime.Composable
34+
import androidx.compose.runtime.LaunchedEffect
35+
import androidx.compose.runtime.getValue
36+
import androidx.compose.runtime.rememberUpdatedState
3337
import androidx.compose.ui.Alignment
3438
import androidx.compose.ui.Modifier
3539
import androidx.compose.ui.res.painterResource
@@ -51,11 +55,13 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight
5155
import io.element.android.libraries.designsystem.theme.components.Button
5256
import io.element.android.libraries.designsystem.theme.components.Text
5357
import io.element.android.libraries.designsystem.theme.components.TextButton
58+
import io.element.android.libraries.designsystem.theme.components.TopAppBar
5459
import io.element.android.libraries.matrix.api.verification.SessionVerificationData
5560
import io.element.android.libraries.matrix.api.verification.VerificationEmoji
5661
import io.element.android.libraries.ui.strings.CommonStrings
5762
import io.element.android.features.verifysession.impl.VerifySelfSessionState.VerificationStep as FlowStep
5863

64+
@OptIn(ExperimentalMaterial3Api::class)
5965
@Composable
6066
fun VerifySelfSessionView(
6167
state: VerifySelfSessionState,
@@ -66,6 +72,12 @@ fun VerifySelfSessionView(
6672
fun resetFlow() {
6773
state.eventSink(VerifySelfSessionViewEvents.Reset)
6874
}
75+
val updatedOnFinished by rememberUpdatedState(newValue = onFinished)
76+
LaunchedEffect(state.verificationFlowStep, updatedOnFinished) {
77+
if (state.verificationFlowStep is FlowStep.Skipped) {
78+
updatedOnFinished()
79+
}
80+
}
6981
BackHandler {
7082
when (state.verificationFlowStep) {
7183
is FlowStep.Canceled -> resetFlow()
@@ -81,6 +93,19 @@ fun VerifySelfSessionView(
8193
val verificationFlowStep = state.verificationFlowStep
8294
HeaderFooterPage(
8395
modifier = modifier,
96+
topBar = {
97+
TopAppBar(
98+
title = {},
99+
actions = {
100+
if (state.displaySkipButton && state.verificationFlowStep != FlowStep.Completed) {
101+
TextButton(
102+
text = stringResource(CommonStrings.action_skip),
103+
onClick = { state.eventSink(VerifySelfSessionViewEvents.SkipVerification) }
104+
)
105+
}
106+
}
107+
)
108+
},
84109
header = {
85110
HeaderContent(verificationFlowStep = verificationFlowStep)
86111
},
@@ -104,6 +129,7 @@ private fun HeaderContent(verificationFlowStep: FlowStep) {
104129
FlowStep.Canceled -> BigIcon.Style.AlertSolid
105130
FlowStep.Ready, is FlowStep.Verifying -> BigIcon.Style.Default(CompoundIcons.Reaction())
106131
FlowStep.Completed -> BigIcon.Style.SuccessSolid
132+
is FlowStep.Skipped -> return
107133
}
108134
val titleTextId = when (verificationFlowStep) {
109135
is FlowStep.Initial, FlowStep.AwaitingOtherDeviceResponse -> R.string.screen_identity_confirmation_title
@@ -114,20 +140,21 @@ private fun HeaderContent(verificationFlowStep: FlowStep) {
114140
is SessionVerificationData.Decimals -> R.string.screen_session_verification_compare_numbers_title
115141
is SessionVerificationData.Emojis -> R.string.screen_session_verification_compare_emojis_title
116142
}
143+
is FlowStep.Skipped -> return
117144
}
118145
val subtitleTextId = when (verificationFlowStep) {
119146
is FlowStep.Initial, FlowStep.AwaitingOtherDeviceResponse -> R.string.screen_identity_confirmation_subtitle
120147
FlowStep.Canceled -> R.string.screen_session_verification_cancelled_subtitle
121148
FlowStep.Ready -> R.string.screen_session_verification_ready_subtitle
122-
FlowStep.Completed -> R.string.screen_identity_confirmation_subtitle
149+
FlowStep.Completed -> R.string.screen_identity_confirmed_subtitle
123150
is FlowStep.Verifying -> when (verificationFlowStep.data) {
124151
is SessionVerificationData.Decimals -> R.string.screen_session_verification_compare_numbers_subtitle
125152
is SessionVerificationData.Emojis -> R.string.screen_session_verification_compare_emojis_subtitle
126153
}
154+
is FlowStep.Skipped -> return
127155
}
128156

129157
PageTitle(
130-
modifier = Modifier.padding(top = 60.dp),
131158
iconStyle = iconStyle,
132159
title = stringResource(id = titleTextId),
133160
subtitle = stringResource(id = subtitleTextId)
@@ -137,9 +164,8 @@ private fun HeaderContent(verificationFlowStep: FlowStep) {
137164
@Composable
138165
private fun Content(flowState: FlowStep) {
139166
Column(Modifier.fillMaxHeight(), verticalArrangement = Arrangement.Center) {
140-
when (flowState) {
141-
is FlowStep.Initial, FlowStep.AwaitingOtherDeviceResponse, FlowStep.Ready, FlowStep.Canceled, FlowStep.Completed -> Unit
142-
is FlowStep.Verifying -> ContentVerifying(flowState)
167+
if (flowState is FlowStep.Verifying) {
168+
ContentVerifying(flowState)
143169
}
144170
}
145171
}
@@ -264,6 +290,7 @@ private fun BottomMenu(
264290
onPositiveButtonClicked = onFinished,
265291
)
266292
}
293+
is FlowStep.Skipped -> return
267294
}
268295
}
269296

features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionViewEvents.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,5 @@ sealed interface VerifySelfSessionViewEvents {
2323
data object DeclineVerification : VerifySelfSessionViewEvents
2424
data object Cancel : VerifySelfSessionViewEvents
2525
data object Reset : VerifySelfSessionViewEvents
26+
data object SkipVerification : VerifySelfSessionViewEvents
2627
}

0 commit comments

Comments
 (0)