diff --git a/features/verifysession/impl/build.gradle.kts b/features/verifysession/impl/build.gradle.kts index 8c1028dabd..2cd2c7b7e3 100644 --- a/features/verifysession/impl/build.gradle.kts +++ b/features/verifysession/impl/build.gradle.kts @@ -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 { 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) diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionNode.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionNode.kt index 0ed9524626..72640d3dbc 100644 --- a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionNode.kt +++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionNode.kt @@ -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 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( @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) }, ) } } diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionPresenter.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionPresenter.kt index ab06dd4915..03da02e4b3 100644 --- a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionPresenter.kt +++ b/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 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( private val stateMachine: VerifySelfSessionStateMachine, private val buildMeta: BuildMeta, private val sessionPreferencesStore: SessionPreferencesStore, + private val logoutUseCase: LogoutUseCase, ) : Presenter { @Composable override fun present(): VerifySelfSessionState { @@ -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.Uninitialized) + } val verificationFlowStep by remember { derivedStateOf { when { @@ -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( } return VerifySelfSessionState( verificationFlowStep = verificationFlowStep, + signOutAction = signOutAction.value, displaySkipButton = buildMeta.isDebuggable, eventSink = ::handleEvents, ) @@ -160,4 +171,10 @@ class VerifySelfSessionPresenter @Inject constructor( } }.launchIn(this) } + + private fun CoroutineScope.signOut(signOutAction: MutableState>) = launch { + suspend { + logoutUseCase.logout(ignoreSdkError = true) + }.runCatchingUpdatingState(signOutAction) + } } diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionState.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionState.kt index 4db21d88b8..aca96eea1c 100644 --- a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionState.kt +++ b/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 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, val displaySkipButton: Boolean, val eventSink: (VerifySelfSessionViewEvents) -> Unit, ) { diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionStateProvider.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionStateProvider.kt index f6db1bc65f..379d9b1686 100644 --- a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionStateProvider.kt +++ b/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 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 = AsyncAction.Uninitialized, displaySkipButton: Boolean = false, eventSink: (VerifySelfSessionViewEvents) -> Unit = {}, ) = VerifySelfSessionState( verificationFlowStep = verificationFlowStep, displaySkipButton = displaySkipButton, eventSink = eventSink, + signOutAction = signOutAction, ) private fun aVerificationEmojiList() = listOf( diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionView.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionView.kt index 446114752e..c3b044a5a1 100644 --- a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionView.kt +++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionView.kt @@ -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 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( 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( 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( ) { 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 onEnterRecoveryKey = {}, onResetKey = {}, onFinish = {}, + onSuccessLogout = {}, ) } diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionViewEvents.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionViewEvents.kt index 2c6b776f7b..59c47e4641 100644 --- a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionViewEvents.kt +++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionViewEvents.kt @@ -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 } diff --git a/features/verifysession/impl/src/main/res/values/localazy.xml b/features/verifysession/impl/src/main/res/values/localazy.xml index d8ce83bfe5..4b5f49a875 100644 --- a/features/verifysession/impl/src/main/res/values/localazy.xml +++ b/features/verifysession/impl/src/main/res/values/localazy.xml @@ -28,4 +28,5 @@ "They match" "Accept the request to start the verification process in your other session to continue." "Waiting to accept request" + "Signing out…" diff --git a/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionPresenterTest.kt b/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionPresenterTest.kt index 6fd13c45bf..c258b9df03 100644 --- a/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionPresenterTest.kt +++ b/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionPresenterTest.kt @@ -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 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 { } } + @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 { "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.requestVerificationAndAwaitVerifyingState( fakeService: FakeSessionVerificationService, sessionVerificationData: SessionVerificationData = SessionVerificationData.Emojis(emptyList()), @@ -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 { stateMachine = VerifySelfSessionStateMachine(service, encryptionService), buildMeta = buildMeta, sessionPreferencesStore = sessionPreferencesStore, + logoutUseCase = logoutUseCase, ) } } diff --git a/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionViewTest.kt b/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionViewTest.kt index 7e5bf928e2..919dba1efd 100644 --- a/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionViewTest.kt +++ b/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionViewTest.kt @@ -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 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 { } } + @Test + fun `on success logout - onFinished callback is called immediately`() { + val aUrl = "aUrl" + ensureCalledOnceWithParam(aUrl) { callback -> + rule.setVerifySelfSessionView( + aVerifySelfSessionState( + signOutAction = AsyncAction.Success(aUrl), + eventSink = EnsureNeverCalledWithParam(), + ), + onSuccessLogout = callback, + ) + } + } + private fun AndroidComposeTestRule.setVerifySelfSessionView( state: VerifySelfSessionState, onEnterRecoveryKey: () -> Unit = EnsureNeverCalled(), onFinished: () -> Unit = EnsureNeverCalled(), onResetKey: () -> Unit = EnsureNeverCalled(), + onSuccessLogout: (String?) -> Unit = EnsureNeverCalledWithParam(), ) { setContent { VerifySelfSessionView( @@ -225,6 +242,7 @@ class VerifySelfSessionViewTest { onEnterRecoveryKey = onEnterRecoveryKey, onFinish = onFinished, onResetKey = onResetKey, + onSuccessLogout = onSuccessLogout, ) } } diff --git a/tools/localazy/config.json b/tools/localazy/config.json index 82fb4f2277..c7b9192a23 100644 --- a/tools/localazy/config.json +++ b/tools/localazy/config.json @@ -55,6 +55,7 @@ "name" : ":features:verifysession:impl", "includeRegex" : [ "screen_session_verification_.*", + "screen_signout_in_progress_dialog_content", "screen_identity_.*" ] },