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 <benoitm+elementbot@element.io>
This commit is contained in:
Jorge Martin Espinosa 2024-04-09 17:28:12 +02:00 committed by GitHub
parent 63f7defb07
commit 1045f99d18
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
44 changed files with 386 additions and 123 deletions

View file

@ -19,11 +19,13 @@ package io.element.android.features.ftue.impl.sessionverification
import android.os.Parcelable
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.lifecycle.lifecycleScope
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import com.bumble.appyx.navmodel.backstack.BackStack
import com.bumble.appyx.navmodel.backstack.operation.newRoot
import com.bumble.appyx.navmodel.backstack.operation.push
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
@ -33,6 +35,7 @@ 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.di.SessionScope
import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize
@ContributesNode(SessionScope::class)
@ -83,7 +86,10 @@ class FtueSessionVerificationFlowNode @AssistedInject constructor(
.params(SecureBackupEntryPoint.Params(SecureBackupEntryPoint.InitialTarget.EnterRecoveryKey))
.callback(object : SecureBackupEntryPoint.Callback {
override fun onDone() {
callback.onDone()
lifecycleScope.launch {
// Move to the completed state view in the verification flow
backstack.newRoot(NavTarget.Root)
}
}
})
.build()

View file

@ -25,7 +25,6 @@ import io.element.android.features.ftue.api.state.FtueState
import io.element.android.features.lockscreen.api.LockScreenService
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
import io.element.android.libraries.permissions.api.PermissionStateProvider
import io.element.android.services.analytics.api.AnalyticsService
import io.element.android.services.toolbox.api.sdk.BuildVersionSdkIntProvider
@ -56,7 +55,7 @@ class DefaultFtueService @Inject constructor(
}
init {
sessionVerificationService.sessionVerifiedStatus
sessionVerificationService.needsVerificationFlow
.onEach { updateState() }
.launchIn(coroutineScope)
@ -99,12 +98,8 @@ class DefaultFtueService @Inject constructor(
).any { it() }
}
private fun isSessionVerificationServiceReady(): Boolean {
return sessionVerificationService.sessionVerifiedStatus.value != SessionVerifiedStatus.Unknown
}
private fun isSessionNotVerified(): Boolean {
return sessionVerificationService.sessionVerifiedStatus.value == SessionVerifiedStatus.NotVerified
return sessionVerificationService.needsVerificationFlow.value
}
private fun needsAnalyticsOptIn(): Boolean {
@ -132,7 +127,6 @@ class DefaultFtueService @Inject constructor(
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal fun updateState() {
state.value = when {
!isSessionVerificationServiceReady() -> FtueState.Unknown
isAnyStepIncomplete() -> FtueState.Incomplete
else -> FtueState.Complete
}

View file

@ -90,6 +90,7 @@ class DefaultFtueServiceTests {
fun `traverse flow`() = runTest {
val sessionVerificationService = FakeSessionVerificationService().apply {
givenVerifiedStatus(SessionVerifiedStatus.NotVerified)
givenNeedsVerification(true)
}
val analyticsService = FakeAnalyticsService()
val permissionStateProvider = FakePermissionStateProvider(permissionGranted = false)
@ -107,7 +108,7 @@ class DefaultFtueServiceTests {
// Session verification
steps.add(state.getNextStep(steps.lastOrNull()))
sessionVerificationService.givenVerifiedStatus(SessionVerifiedStatus.Verified)
sessionVerificationService.givenNeedsVerification(false)
// Notifications opt in
steps.add(state.getNextStep(steps.lastOrNull()))

View file

@ -38,6 +38,7 @@ fun aSignedOutState() = SignedOutState(
fun aSessionData(
sessionId: SessionId = SessionId("@alice:server.org"),
isTokenValid: Boolean = false,
needsVerification: Boolean = false,
): SessionData {
return SessionData(
userId = sessionId.value,
@ -51,5 +52,6 @@ fun aSessionData(
isTokenValid = isTokenValid,
loginType = LoginType.UNKNOWN,
passphrase = null,
needsVerification = needsVerification,
)
}

View file

@ -23,10 +23,14 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import com.freeletics.flowredux.compose.rememberStateAndDispatch
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.matrix.api.encryption.EncryptionService
import io.element.android.libraries.matrix.api.encryption.RecoveryState
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
@ -35,6 +39,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import javax.inject.Inject
import io.element.android.features.verifysession.impl.VerifySelfSessionStateMachine.Event as StateMachineEvent
import io.element.android.features.verifysession.impl.VerifySelfSessionStateMachine.State as StateMachineState
@ -43,20 +48,28 @@ class VerifySelfSessionPresenter @Inject constructor(
private val sessionVerificationService: SessionVerificationService,
private val encryptionService: EncryptionService,
private val stateMachine: VerifySelfSessionStateMachine,
private val buildMeta: BuildMeta,
) : Presenter<VerifySelfSessionState> {
@Composable
override fun present(): VerifySelfSessionState {
val coroutineScope = rememberCoroutineScope()
LaunchedEffect(Unit) {
// Force reset, just in case the service was left in a broken state
sessionVerificationService.reset()
}
val recoveryState by encryptionService.recoveryStateStateFlow.collectAsState()
val stateAndDispatch = stateMachine.rememberStateAndDispatch()
var skipVerification by remember { mutableStateOf(false) }
val needsVerification by sessionVerificationService.needsVerificationFlow.collectAsState()
val verificationFlowStep by remember {
derivedStateOf {
stateAndDispatch.state.value.toVerificationStep(
canEnterRecoveryKey = recoveryState == RecoveryState.INCOMPLETE
)
when {
skipVerification -> VerifySelfSessionState.VerificationStep.Skipped
needsVerification -> stateAndDispatch.state.value.toVerificationStep(
canEnterRecoveryKey = recoveryState == RecoveryState.INCOMPLETE
)
else -> VerifySelfSessionState.VerificationStep.Completed
}
}
}
// Start this after observing state machine
@ -72,10 +85,15 @@ class VerifySelfSessionPresenter @Inject constructor(
VerifySelfSessionViewEvents.DeclineVerification -> stateAndDispatch.dispatchAction(StateMachineEvent.DeclineChallenge)
VerifySelfSessionViewEvents.Cancel -> stateAndDispatch.dispatchAction(StateMachineEvent.Cancel)
VerifySelfSessionViewEvents.Reset -> stateAndDispatch.dispatchAction(StateMachineEvent.Reset)
VerifySelfSessionViewEvents.SkipVerification -> coroutineScope.launch {
sessionVerificationService.saveVerifiedState(true)
skipVerification = true
}
}
}
return VerifySelfSessionState(
verificationFlowStep = verificationFlowStep,
displaySkipButton = buildMeta.isDebuggable,
eventSink = ::handleEvents,
)
}

View file

@ -24,6 +24,7 @@ import io.element.android.libraries.matrix.api.verification.SessionVerificationD
@Immutable
data class VerifySelfSessionState(
val verificationFlowStep: VerificationStep,
val displaySkipButton: Boolean,
val eventSink: (VerifySelfSessionViewEvents) -> Unit,
) {
@Stable
@ -34,5 +35,6 @@ data class VerifySelfSessionState(
data object Ready : VerificationStep
data class Verifying(val data: SessionVerificationData, val state: AsyncData<Unit>) : VerificationStep
data object Completed : VerificationStep
data object Skipped : VerificationStep
}
}

View file

@ -24,7 +24,7 @@ import io.element.android.libraries.matrix.api.verification.VerificationEmoji
open class VerifySelfSessionStateProvider : PreviewParameterProvider<VerifySelfSessionState> {
override val values: Sequence<VerifySelfSessionState>
get() = sequenceOf(
aVerifySelfSessionState(),
aVerifySelfSessionState(displaySkipButton = true),
aVerifySelfSessionState(
verificationFlowStep = VerifySelfSessionState.VerificationStep.AwaitingOtherDeviceResponse
),
@ -46,6 +46,10 @@ open class VerifySelfSessionStateProvider : PreviewParameterProvider<VerifySelfS
aVerifySelfSessionState(
verificationFlowStep = VerifySelfSessionState.VerificationStep.Initial(true)
),
aVerifySelfSessionState(
verificationFlowStep = VerifySelfSessionState.VerificationStep.Completed,
displaySkipButton = true,
),
// Add other state here
)
}
@ -64,9 +68,11 @@ private fun aDecimalsSessionVerificationData(
internal fun aVerifySelfSessionState(
verificationFlowStep: VerifySelfSessionState.VerificationStep = VerifySelfSessionState.VerificationStep.Initial(false),
displaySkipButton: Boolean = false,
eventSink: (VerifySelfSessionViewEvents) -> Unit = {},
) = VerifySelfSessionState(
verificationFlowStep = verificationFlowStep,
displaySkipButton = displaySkipButton,
eventSink = eventSink,
)

View file

@ -28,8 +28,12 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.widthIn
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
@ -51,11 +55,13 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Button
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.components.TextButton
import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.matrix.api.verification.SessionVerificationData
import io.element.android.libraries.matrix.api.verification.VerificationEmoji
import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.features.verifysession.impl.VerifySelfSessionState.VerificationStep as FlowStep
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun VerifySelfSessionView(
state: VerifySelfSessionState,
@ -66,6 +72,12 @@ fun VerifySelfSessionView(
fun resetFlow() {
state.eventSink(VerifySelfSessionViewEvents.Reset)
}
val updatedOnFinished by rememberUpdatedState(newValue = onFinished)
LaunchedEffect(state.verificationFlowStep, updatedOnFinished) {
if (state.verificationFlowStep is FlowStep.Skipped) {
updatedOnFinished()
}
}
BackHandler {
when (state.verificationFlowStep) {
is FlowStep.Canceled -> resetFlow()
@ -81,6 +93,19 @@ fun VerifySelfSessionView(
val verificationFlowStep = state.verificationFlowStep
HeaderFooterPage(
modifier = modifier,
topBar = {
TopAppBar(
title = {},
actions = {
if (state.displaySkipButton && state.verificationFlowStep != FlowStep.Completed) {
TextButton(
text = stringResource(CommonStrings.action_skip),
onClick = { state.eventSink(VerifySelfSessionViewEvents.SkipVerification) }
)
}
}
)
},
header = {
HeaderContent(verificationFlowStep = verificationFlowStep)
},
@ -104,6 +129,7 @@ private fun HeaderContent(verificationFlowStep: FlowStep) {
FlowStep.Canceled -> BigIcon.Style.AlertSolid
FlowStep.Ready, is FlowStep.Verifying -> BigIcon.Style.Default(CompoundIcons.Reaction())
FlowStep.Completed -> BigIcon.Style.SuccessSolid
is FlowStep.Skipped -> return
}
val titleTextId = when (verificationFlowStep) {
is FlowStep.Initial, FlowStep.AwaitingOtherDeviceResponse -> R.string.screen_identity_confirmation_title
@ -114,20 +140,21 @@ private fun HeaderContent(verificationFlowStep: FlowStep) {
is SessionVerificationData.Decimals -> R.string.screen_session_verification_compare_numbers_title
is SessionVerificationData.Emojis -> R.string.screen_session_verification_compare_emojis_title
}
is FlowStep.Skipped -> return
}
val subtitleTextId = when (verificationFlowStep) {
is FlowStep.Initial, FlowStep.AwaitingOtherDeviceResponse -> R.string.screen_identity_confirmation_subtitle
FlowStep.Canceled -> R.string.screen_session_verification_cancelled_subtitle
FlowStep.Ready -> R.string.screen_session_verification_ready_subtitle
FlowStep.Completed -> R.string.screen_identity_confirmation_subtitle
FlowStep.Completed -> R.string.screen_identity_confirmed_subtitle
is FlowStep.Verifying -> when (verificationFlowStep.data) {
is SessionVerificationData.Decimals -> R.string.screen_session_verification_compare_numbers_subtitle
is SessionVerificationData.Emojis -> R.string.screen_session_verification_compare_emojis_subtitle
}
is FlowStep.Skipped -> return
}
PageTitle(
modifier = Modifier.padding(top = 60.dp),
iconStyle = iconStyle,
title = stringResource(id = titleTextId),
subtitle = stringResource(id = subtitleTextId)
@ -137,9 +164,8 @@ private fun HeaderContent(verificationFlowStep: FlowStep) {
@Composable
private fun Content(flowState: FlowStep) {
Column(Modifier.fillMaxHeight(), verticalArrangement = Arrangement.Center) {
when (flowState) {
is FlowStep.Initial, FlowStep.AwaitingOtherDeviceResponse, FlowStep.Ready, FlowStep.Canceled, FlowStep.Completed -> Unit
is FlowStep.Verifying -> ContentVerifying(flowState)
if (flowState is FlowStep.Verifying) {
ContentVerifying(flowState)
}
}
}
@ -264,6 +290,7 @@ private fun BottomMenu(
onPositiveButtonClicked = onFinished,
)
}
is FlowStep.Skipped -> return
}
}

View file

@ -23,4 +23,5 @@ sealed interface VerifySelfSessionViewEvents {
data object DeclineVerification : VerifySelfSessionViewEvents
data object Cancel : VerifySelfSessionViewEvents
data object Reset : VerifySelfSessionViewEvents
data object SkipVerification : VerifySelfSessionViewEvents
}

View file

@ -23,15 +23,19 @@ import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.features.verifysession.impl.VerifySelfSessionState.VerificationStep
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.matrix.api.encryption.EncryptionService
import io.element.android.libraries.matrix.api.encryption.RecoveryState
import io.element.android.libraries.matrix.api.verification.SessionVerificationData
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
import io.element.android.libraries.matrix.api.verification.VerificationEmoji
import io.element.android.libraries.matrix.api.verification.VerificationFlowState
import io.element.android.libraries.matrix.test.core.aBuildMeta
import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService
import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService
import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.lambda.value
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Rule
@ -48,7 +52,21 @@ class VerifySelfSessionPresenterTests {
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.Initial(false))
awaitItem().run {
assertThat(verificationFlowStep).isEqualTo(VerificationStep.Initial(false))
assertThat(displaySkipButton).isTrue()
}
}
}
@Test
fun `present - hides skip verification button on non-debuggable builds`() = runTest {
val buildMeta = aBuildMeta(isDebuggable = false)
val presenter = createVerifySelfSessionPresenter(buildMeta = buildMeta)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
assertThat(awaitItem().displaySkipButton).isFalse()
}
}
@ -68,7 +86,7 @@ class VerifySelfSessionPresenterTests {
@Test
fun `present - Handles requestVerification`() = runTest {
val service = FakeSessionVerificationService()
val service = unverifiedSessionService()
val presenter = createVerifySelfSessionPresenter(service)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
@ -79,7 +97,7 @@ class VerifySelfSessionPresenterTests {
@Test
fun `present - Handles startSasVerification`() = runTest {
val service = FakeSessionVerificationService()
val service = unverifiedSessionService()
val presenter = createVerifySelfSessionPresenter(service)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
@ -113,7 +131,7 @@ class VerifySelfSessionPresenterTests {
@Test
fun `present - A failure when verifying cancels it`() = runTest {
val service = FakeSessionVerificationService()
val service = unverifiedSessionService()
val presenter = createVerifySelfSessionPresenter(service)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
@ -130,7 +148,7 @@ class VerifySelfSessionPresenterTests {
@Test
fun `present - A fail when requesting verification resets the state to the initial one`() = runTest {
val service = FakeSessionVerificationService()
val service = unverifiedSessionService()
val presenter = createVerifySelfSessionPresenter(service)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
@ -145,7 +163,7 @@ class VerifySelfSessionPresenterTests {
@Test
fun `present - Canceling the flow once it's verifying cancels it`() = runTest {
val service = FakeSessionVerificationService()
val service = unverifiedSessionService()
val presenter = createVerifySelfSessionPresenter(service)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
@ -158,7 +176,7 @@ class VerifySelfSessionPresenterTests {
@Test
fun `present - When verifying, if we receive another challenge we ignore it`() = runTest {
val service = FakeSessionVerificationService()
val service = unverifiedSessionService()
val presenter = createVerifySelfSessionPresenter(service)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
@ -171,7 +189,7 @@ class VerifySelfSessionPresenterTests {
@Test
fun `present - Restart after cancelation returns to requesting verification`() = runTest {
val service = FakeSessionVerificationService()
val service = unverifiedSessionService()
val presenter = createVerifySelfSessionPresenter(service)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
@ -188,7 +206,7 @@ class VerifySelfSessionPresenterTests {
@Test
fun `present - Go back after cancelation returns to initial state`() = runTest {
val service = FakeSessionVerificationService()
val service = unverifiedSessionService()
val presenter = createVerifySelfSessionPresenter(service)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
@ -208,7 +226,7 @@ class VerifySelfSessionPresenterTests {
val emojis = listOf(
VerificationEmoji(number = 30, emoji = "😀", description = "Smiley")
)
val service = FakeSessionVerificationService()
val service = unverifiedSessionService()
val presenter = createVerifySelfSessionPresenter(service)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
@ -230,7 +248,7 @@ class VerifySelfSessionPresenterTests {
@Test
fun `present - When verification is declined, the flow is canceled`() = runTest {
val service = FakeSessionVerificationService()
val service = unverifiedSessionService()
val presenter = createVerifySelfSessionPresenter(service)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
@ -247,6 +265,33 @@ class VerifySelfSessionPresenterTests {
}
}
@Test
fun `present - Skip event skips the flow`() = runTest {
val service = unverifiedSessionService()
val presenter = createVerifySelfSessionPresenter(service)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val state = requestVerificationAndAwaitVerifyingState(service)
state.eventSink(VerifySelfSessionViewEvents.SkipVerification)
service.saveVerifiedStateResult.assertions().isCalledOnce().with(value(true))
assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.Skipped)
}
}
@Test
fun `present - When verification is not needed, the flow is completed`() = runTest {
val service = FakeSessionVerificationService().apply {
givenNeedsVerification(false)
}
val presenter = createVerifySelfSessionPresenter(service)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.Completed)
}
}
private suspend fun ReceiveTurbine<VerifySelfSessionState>.requestVerificationAndAwaitVerifyingState(
fakeService: FakeSessionVerificationService,
sessionVerificationData: SessionVerificationData = SessionVerificationData.Emojis(emptyList()),
@ -271,14 +316,23 @@ class VerifySelfSessionPresenterTests {
return state
}
private fun unverifiedSessionService(): FakeSessionVerificationService {
return FakeSessionVerificationService().apply {
givenVerifiedStatus(SessionVerifiedStatus.NotVerified)
givenNeedsVerification(true)
}
}
private fun createVerifySelfSessionPresenter(
service: SessionVerificationService = FakeSessionVerificationService(),
service: SessionVerificationService = unverifiedSessionService(),
encryptionService: EncryptionService = FakeEncryptionService(),
buildMeta: BuildMeta = aBuildMeta(),
): VerifySelfSessionPresenter {
return VerifySelfSessionPresenter(
sessionVerificationService = service,
encryptionService = encryptionService,
stateMachine = VerifySelfSessionStateMachine(service, encryptionService),
buildMeta = buildMeta,
)
}
}

View file

@ -22,6 +22,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.tests.testutils.EnsureNeverCalled
import io.element.android.tests.testutils.EnsureNeverCalledWithParam
import io.element.android.tests.testutils.EventsRecorder
import io.element.android.tests.testutils.clickOn
import io.element.android.tests.testutils.ensureCalledOnce
@ -126,6 +127,23 @@ class VerifySelfSessionViewTest {
eventsRecorder.assertEmpty()
}
@Test
fun `back key pressed - on Completed step does nothing`() {
val eventsRecorder = EventsRecorder<VerifySelfSessionViewEvents>()
rule.setContent {
VerifySelfSessionView(
aVerifySelfSessionState(
verificationFlowStep = VerifySelfSessionState.VerificationStep.Completed,
eventSink = eventsRecorder
),
onEnterRecoveryKey = EnsureNeverCalled(),
onFinished = EnsureNeverCalled(),
)
}
rule.pressBackKey()
eventsRecorder.assertEmpty()
}
@Test
fun `when flow is completed and the user clicks on the continue button, the expected callback is invoked`() {
val eventsRecorder = EventsRecorder<VerifySelfSessionViewEvents>(expectEvents = false)
@ -202,4 +220,39 @@ class VerifySelfSessionViewTest {
rule.clickOn(R.string.screen_session_verification_they_dont_match)
eventsRecorder.assertSingle(VerifySelfSessionViewEvents.DeclineVerification)
}
@Test
fun `clicking on 'Skip' emits the expected event`() {
val eventsRecorder = EventsRecorder<VerifySelfSessionViewEvents>()
rule.setContent {
VerifySelfSessionView(
aVerifySelfSessionState(
verificationFlowStep = VerifySelfSessionState.VerificationStep.Initial(canEnterRecoveryKey = true),
displaySkipButton = true,
eventSink = eventsRecorder
),
onEnterRecoveryKey = EnsureNeverCalled(),
onFinished = EnsureNeverCalled(),
)
}
rule.clickOn(CommonStrings.action_skip)
eventsRecorder.assertSingle(VerifySelfSessionViewEvents.SkipVerification)
}
@Test
fun `on Skipped step - onFinished callback is called immediately`() {
ensureCalledOnce { callback ->
rule.setContent {
VerifySelfSessionView(
aVerifySelfSessionState(
verificationFlowStep = VerifySelfSessionState.VerificationStep.Skipped,
displaySkipButton = true,
eventSink = EnsureNeverCalledWithParam(),
),
onEnterRecoveryKey = EnsureNeverCalled(),
onFinished = callback,
)
}
}
}
}