Browse Source

Add a way to sign out when the user is asked to verify the session.

pull/3359/head
Benoit Marty 3 weeks ago
parent
commit
f4beddef99
  1. 2
      features/verifysession/impl/build.gradle.kts
  2. 7
      features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionNode.kt
  3. 17
      features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionPresenter.kt
  4. 2
      features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionState.kt
  5. 7
      features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionStateProvider.kt
  6. 49
      features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionView.kt
  7. 1
      features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionViewEvents.kt
  8. 1
      features/verifysession/impl/src/main/res/values/localazy.xml
  9. 31
      features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionPresenterTest.kt
  10. 18
      features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionViewTest.kt
  11. 1
      tools/localazy/config.json

2
features/verifysession/impl/build.gradle.kts

@ -43,6 +43,7 @@ dependencies { @@ -43,6 +43,7 @@ dependencies {
implementation(projects.libraries.designsystem)
implementation(projects.libraries.preferences.api)
implementation(projects.libraries.uiStrings)
implementation(projects.features.logout.api)
api(libs.statemachine)
api(projects.features.verifysession.api)
@ -52,6 +53,7 @@ dependencies { @@ -52,6 +53,7 @@ dependencies {
testImplementation(libs.test.robolectric)
testImplementation(libs.test.truth)
testImplementation(libs.test.turbine)
testImplementation(projects.features.logout.test)
testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.preferences.test)
testImplementation(projects.tests.testutils)

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

@ -16,8 +16,10 @@ @@ -16,8 +16,10 @@
package io.element.android.features.verifysession.impl
import android.app.Activity
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
@ -25,6 +27,8 @@ import com.bumble.appyx.core.plugin.plugins @@ -25,6 +27,8 @@ import com.bumble.appyx.core.plugin.plugins
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode
import io.element.android.compound.theme.ElementTheme
import io.element.android.features.logout.api.util.onSuccessLogout
import io.element.android.features.verifysession.api.VerifySessionEntryPoint
import io.element.android.libraries.di.SessionScope
@ -39,12 +43,15 @@ class VerifySelfSessionNode @AssistedInject constructor( @@ -39,12 +43,15 @@ class VerifySelfSessionNode @AssistedInject constructor(
@Composable
override fun View(modifier: Modifier) {
val state = presenter.present()
val activity = LocalContext.current as Activity
val isDark = ElementTheme.isLightTheme.not()
VerifySelfSessionView(
state = state,
modifier = modifier,
onEnterRecoveryKey = callback::onEnterRecoveryKey,
onResetKey = callback::onResetKey,
onFinish = callback::onDone,
onSuccessLogout = { onSuccessLogout(activity, isDark, it) },
)
}
}

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

@ -20,14 +20,19 @@ package io.element.android.features.verifysession.impl @@ -20,14 +20,19 @@ package io.element.android.features.verifysession.impl
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
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 com.freeletics.flowredux.compose.rememberStateAndDispatch
import io.element.android.features.logout.api.LogoutUseCase
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.runCatchingUpdatingState
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
@ -49,6 +54,7 @@ class VerifySelfSessionPresenter @Inject constructor( @@ -49,6 +54,7 @@ class VerifySelfSessionPresenter @Inject constructor(
private val stateMachine: VerifySelfSessionStateMachine,
private val buildMeta: BuildMeta,
private val sessionPreferencesStore: SessionPreferencesStore,
private val logoutUseCase: LogoutUseCase,
) : Presenter<VerifySelfSessionState> {
@Composable
override fun present(): VerifySelfSessionState {
@ -61,6 +67,9 @@ class VerifySelfSessionPresenter @Inject constructor( @@ -61,6 +67,9 @@ class VerifySelfSessionPresenter @Inject constructor(
val stateAndDispatch = stateMachine.rememberStateAndDispatch()
val skipVerification by sessionPreferencesStore.isSessionVerificationSkipped().collectAsState(initial = false)
val needsVerification by sessionVerificationService.needsSessionVerification.collectAsState(initial = true)
val signOutAction = remember {
mutableStateOf<AsyncAction<String?>>(AsyncAction.Uninitialized)
}
val verificationFlowStep by remember {
derivedStateOf {
when {
@ -85,6 +94,7 @@ class VerifySelfSessionPresenter @Inject constructor( @@ -85,6 +94,7 @@ class VerifySelfSessionPresenter @Inject constructor(
VerifySelfSessionViewEvents.DeclineVerification -> stateAndDispatch.dispatchAction(StateMachineEvent.DeclineChallenge)
VerifySelfSessionViewEvents.Cancel -> stateAndDispatch.dispatchAction(StateMachineEvent.Cancel)
VerifySelfSessionViewEvents.Reset -> stateAndDispatch.dispatchAction(StateMachineEvent.Reset)
VerifySelfSessionViewEvents.SignOut -> coroutineScope.signOut(signOutAction)
VerifySelfSessionViewEvents.SkipVerification -> coroutineScope.launch {
sessionPreferencesStore.setSkipSessionVerification(true)
}
@ -92,6 +102,7 @@ class VerifySelfSessionPresenter @Inject constructor( @@ -92,6 +102,7 @@ class VerifySelfSessionPresenter @Inject constructor(
}
return VerifySelfSessionState(
verificationFlowStep = verificationFlowStep,
signOutAction = signOutAction.value,
displaySkipButton = buildMeta.isDebuggable,
eventSink = ::handleEvents,
)
@ -160,4 +171,10 @@ class VerifySelfSessionPresenter @Inject constructor( @@ -160,4 +171,10 @@ class VerifySelfSessionPresenter @Inject constructor(
}
}.launchIn(this)
}
private fun CoroutineScope.signOut(signOutAction: MutableState<AsyncAction<String?>>) = launch {
suspend {
logoutUseCase.logout(ignoreSdkError = true)
}.runCatchingUpdatingState(signOutAction)
}
}

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

@ -18,12 +18,14 @@ package io.element.android.features.verifysession.impl @@ -18,12 +18,14 @@ package io.element.android.features.verifysession.impl
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.matrix.api.verification.SessionVerificationData
@Immutable
data class VerifySelfSessionState(
val verificationFlowStep: VerificationStep,
val signOutAction: AsyncAction<String?>,
val displaySkipButton: Boolean,
val eventSink: (VerifySelfSessionViewEvents) -> Unit,
) {

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

@ -18,6 +18,7 @@ package io.element.android.features.verifysession.impl @@ -18,6 +18,7 @@ package io.element.android.features.verifysession.impl
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.verifysession.impl.VerifySelfSessionState.VerificationStep
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.matrix.api.verification.SessionVerificationData
import io.element.android.libraries.matrix.api.verification.VerificationEmoji
@ -54,6 +55,10 @@ open class VerifySelfSessionStateProvider : PreviewParameterProvider<VerifySelfS @@ -54,6 +55,10 @@ open class VerifySelfSessionStateProvider : PreviewParameterProvider<VerifySelfS
verificationFlowStep = VerificationStep.Completed,
displaySkipButton = true,
),
aVerifySelfSessionState(
signOutAction = AsyncAction.Loading,
displaySkipButton = true,
),
// Add other state here
)
}
@ -72,12 +77,14 @@ private fun aDecimalsSessionVerificationData( @@ -72,12 +77,14 @@ private fun aDecimalsSessionVerificationData(
internal fun aVerifySelfSessionState(
verificationFlowStep: VerificationStep = VerificationStep.Initial(canEnterRecoveryKey = false),
signOutAction: AsyncAction<String?> = AsyncAction.Uninitialized,
displaySkipButton: Boolean = false,
eventSink: (VerifySelfSessionViewEvents) -> Unit = {},
) = VerifySelfSessionState(
verificationFlowStep = verificationFlowStep,
displaySkipButton = displaySkipButton,
eventSink = eventSink,
signOutAction = signOutAction,
)
private fun aVerificationEmojiList() = listOf(

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

@ -37,6 +37,7 @@ import androidx.compose.runtime.getValue @@ -37,6 +37,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
@ -46,11 +47,13 @@ import androidx.compose.ui.unit.dp @@ -46,11 +47,13 @@ import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.verifysession.impl.emoji.toEmojiResource
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
import io.element.android.libraries.designsystem.components.BigIcon
import io.element.android.libraries.designsystem.components.PageTitle
import io.element.android.libraries.designsystem.components.ProgressDialog
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Button
@ -70,11 +73,13 @@ fun VerifySelfSessionView( @@ -70,11 +73,13 @@ fun VerifySelfSessionView(
onEnterRecoveryKey: () -> Unit,
onResetKey: () -> Unit,
onFinish: () -> Unit,
onSuccessLogout: (String?) -> Unit,
modifier: Modifier = Modifier,
) {
fun resetFlow() {
state.eventSink(VerifySelfSessionViewEvents.Reset)
}
val latestOnFinish by rememberUpdatedState(newValue = onFinish)
LaunchedEffect(state.verificationFlowStep, latestOnFinish) {
if (state.verificationFlowStep is FlowStep.Skipped) {
@ -97,17 +102,23 @@ fun VerifySelfSessionView( @@ -97,17 +102,23 @@ fun VerifySelfSessionView(
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) }
)
}
}
)
TopAppBar(
title = {},
actions = {
if (state.verificationFlowStep != FlowStep.Completed) {
if (state.displaySkipButton && LocalInspectionMode.current.not()) {
TextButton(
text = stringResource(CommonStrings.action_skip),
onClick = { state.eventSink(VerifySelfSessionViewEvents.SkipVerification) }
)
}
TextButton(
text = stringResource(CommonStrings.action_signout),
onClick = { state.eventSink(VerifySelfSessionViewEvents.SignOut) }
)
}
}
)
},
header = {
HeaderContent(verificationFlowStep = verificationFlowStep)
@ -124,6 +135,21 @@ fun VerifySelfSessionView( @@ -124,6 +135,21 @@ fun VerifySelfSessionView(
) {
Content(flowState = verificationFlowStep)
}
when (state.signOutAction) {
AsyncAction.Loading -> {
ProgressDialog(text = stringResource(id = R.string.screen_signout_in_progress_dialog_content))
}
is AsyncAction.Success -> {
val latestOnSuccessLogout by rememberUpdatedState(onSuccessLogout)
LaunchedEffect(state) {
latestOnSuccessLogout(state.signOutAction.data)
}
}
AsyncAction.Confirming,
is AsyncAction.Failure,
AsyncAction.Uninitialized -> Unit
}
}
@Composable
@ -367,5 +393,6 @@ internal fun VerifySelfSessionViewPreview(@PreviewParameter(VerifySelfSessionSta @@ -367,5 +393,6 @@ internal fun VerifySelfSessionViewPreview(@PreviewParameter(VerifySelfSessionSta
onEnterRecoveryKey = {},
onResetKey = {},
onFinish = {},
onSuccessLogout = {},
)
}

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

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

1
features/verifysession/impl/src/main/res/values/localazy.xml

@ -28,4 +28,5 @@ @@ -28,4 +28,5 @@
<string name="screen_session_verification_they_match">"They match"</string>
<string name="screen_session_verification_waiting_to_accept_subtitle">"Accept the request to start the verification process in your other session to continue."</string>
<string name="screen_session_verification_waiting_to_accept_title">"Waiting to accept request"</string>
<string name="screen_signout_in_progress_dialog_content">"Signing out…"</string>
</resources>

31
features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionPresenterTest.kt

@ -21,6 +21,8 @@ import app.cash.molecule.moleculeFlow @@ -21,6 +21,8 @@ import app.cash.molecule.moleculeFlow
import app.cash.turbine.ReceiveTurbine
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.features.logout.api.LogoutUseCase
import io.element.android.features.logout.test.FakeLogoutUseCase
import io.element.android.features.verifysession.impl.VerifySelfSessionState.VerificationStep
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.core.meta.BuildMeta
@ -36,6 +38,8 @@ import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService @@ -36,6 +38,8 @@ import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService
import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService
import io.element.android.libraries.preferences.test.InMemorySessionPreferencesStore
import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.lambda.value
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Rule
@ -309,6 +313,31 @@ class VerifySelfSessionPresenterTest { @@ -309,6 +313,31 @@ class VerifySelfSessionPresenterTest {
}
}
@Test
fun `present - When user request to sign out, the sign out use case is invoked`() = runTest {
val service = FakeSessionVerificationService().apply {
givenNeedsSessionVerification(false)
givenVerifiedStatus(SessionVerifiedStatus.Verified)
givenVerificationFlowState(VerificationFlowState.Finished)
}
val signOutLambda = lambdaRecorder<Boolean, String?> { "aUrl" }
val presenter = createVerifySelfSessionPresenter(
service,
logoutUseCase = FakeLogoutUseCase(signOutLambda)
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
skipItems(1)
val initialItem = awaitItem()
initialItem.eventSink(VerifySelfSessionViewEvents.SignOut)
val finalItem = awaitItem()
assertThat(finalItem.signOutAction.isSuccess()).isTrue()
assertThat(finalItem.signOutAction.dataOrNull()).isEqualTo("aUrl")
signOutLambda.assertions().isCalledOnce().with(value(true))
}
}
private suspend fun ReceiveTurbine<VerifySelfSessionState>.requestVerificationAndAwaitVerifyingState(
fakeService: FakeSessionVerificationService,
sessionVerificationData: SessionVerificationData = SessionVerificationData.Emojis(emptyList()),
@ -344,6 +373,7 @@ class VerifySelfSessionPresenterTest { @@ -344,6 +373,7 @@ class VerifySelfSessionPresenterTest {
encryptionService: EncryptionService = FakeEncryptionService(),
buildMeta: BuildMeta = aBuildMeta(),
sessionPreferencesStore: InMemorySessionPreferencesStore = InMemorySessionPreferencesStore(),
logoutUseCase: LogoutUseCase = FakeLogoutUseCase(),
): VerifySelfSessionPresenter {
return VerifySelfSessionPresenter(
sessionVerificationService = service,
@ -351,6 +381,7 @@ class VerifySelfSessionPresenterTest { @@ -351,6 +381,7 @@ class VerifySelfSessionPresenterTest {
stateMachine = VerifySelfSessionStateMachine(service, encryptionService),
buildMeta = buildMeta,
sessionPreferencesStore = sessionPreferencesStore,
logoutUseCase = logoutUseCase,
)
}
}

18
features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionViewTest.kt

@ -20,6 +20,7 @@ import androidx.activity.ComponentActivity @@ -20,6 +20,7 @@ import androidx.activity.ComponentActivity
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.tests.testutils.EnsureNeverCalled
@ -27,6 +28,7 @@ import io.element.android.tests.testutils.EnsureNeverCalledWithParam @@ -27,6 +28,7 @@ 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
import io.element.android.tests.testutils.ensureCalledOnceWithParam
import io.element.android.tests.testutils.pressBackKey
import org.junit.Rule
import org.junit.Test
@ -213,11 +215,26 @@ class VerifySelfSessionViewTest { @@ -213,11 +215,26 @@ class VerifySelfSessionViewTest {
}
}
@Test
fun `on success logout - onFinished callback is called immediately`() {
val aUrl = "aUrl"
ensureCalledOnceWithParam<String?>(aUrl) { callback ->
rule.setVerifySelfSessionView(
aVerifySelfSessionState(
signOutAction = AsyncAction.Success(aUrl),
eventSink = EnsureNeverCalledWithParam(),
),
onSuccessLogout = callback,
)
}
}
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setVerifySelfSessionView(
state: VerifySelfSessionState,
onEnterRecoveryKey: () -> Unit = EnsureNeverCalled(),
onFinished: () -> Unit = EnsureNeverCalled(),
onResetKey: () -> Unit = EnsureNeverCalled(),
onSuccessLogout: (String?) -> Unit = EnsureNeverCalledWithParam(),
) {
setContent {
VerifySelfSessionView(
@ -225,6 +242,7 @@ class VerifySelfSessionViewTest { @@ -225,6 +242,7 @@ class VerifySelfSessionViewTest {
onEnterRecoveryKey = onEnterRecoveryKey,
onFinish = onFinished,
onResetKey = onResetKey,
onSuccessLogout = onSuccessLogout,
)
}
}

1
tools/localazy/config.json

@ -55,6 +55,7 @@ @@ -55,6 +55,7 @@
"name" : ":features:verifysession:impl",
"includeRegex" : [
"screen_session_verification_.*",
"screen_signout_in_progress_dialog_content",
"screen_identity_.*"
]
},

Loading…
Cancel
Save