When logging out from Pin code screen, logout from all the sessions.

This commit is contained in:
Benoit Marty 2025-09-18 09:44:02 +02:00
parent a2d728d29e
commit be61b89bfb
6 changed files with 146 additions and 20 deletions

View file

@ -174,7 +174,7 @@ class PinUnlockPresenter(
private fun CoroutineScope.signOut(signOutAction: MutableState<AsyncAction<Unit>>) = launch { private fun CoroutineScope.signOut(signOutAction: MutableState<AsyncAction<Unit>>) = launch {
suspend { suspend {
logoutUseCase.logout(ignoreSdkError = true) logoutUseCase.logoutAll(ignoreSdkError = true)
}.runCatchingUpdatingState(signOutAction) }.runCatchingUpdatingState(signOutAction)
} }
} }

View file

@ -8,16 +8,12 @@
package io.element.android.features.logout.api package io.element.android.features.logout.api
/** /**
* Used to trigger a log out of the current user from any part of the app. * Used to trigger a log out of the current user(s) from any part of the app.
*/ */
interface LogoutUseCase { interface LogoutUseCase {
/** /**
* Log out the current user and then perform any needed cleanup tasks. * Log out the current user(s) and then perform any needed cleanup tasks.
* @param ignoreSdkError if true, the SDK error will be ignored and the user will be logged out anyway. * @param ignoreSdkError if true, the SDK error will be ignored and the user will be logged out anyway.
*/ */
suspend fun logout(ignoreSdkError: Boolean) suspend fun logoutAll(ignoreSdkError: Boolean)
interface Factory {
fun create(sessionId: String): LogoutUseCase
}
} }

View file

@ -39,4 +39,5 @@ dependencies {
testCommonDependencies(libs, true) testCommonDependencies(libs, true)
testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.featureflag.test) testImplementation(projects.libraries.featureflag.test)
testImplementation(projects.libraries.sessionStorage.test)
} }

View file

@ -12,22 +12,31 @@ import dev.zacsweers.metro.ContributesBinding
import dev.zacsweers.metro.Inject import dev.zacsweers.metro.Inject
import io.element.android.features.logout.api.LogoutUseCase import io.element.android.features.logout.api.LogoutUseCase
import io.element.android.libraries.matrix.api.MatrixClientProvider import io.element.android.libraries.matrix.api.MatrixClientProvider
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.sessionstorage.api.SessionStore
import timber.log.Timber
@ContributesBinding(AppScope::class) @ContributesBinding(AppScope::class)
@Inject @Inject
class DefaultLogoutUseCase( class DefaultLogoutUseCase(
private val authenticationService: MatrixAuthenticationService, private val sessionStore: SessionStore,
private val matrixClientProvider: MatrixClientProvider, private val matrixClientProvider: MatrixClientProvider,
) : LogoutUseCase { ) : LogoutUseCase {
override suspend fun logout(ignoreSdkError: Boolean) { override suspend fun logoutAll(ignoreSdkError: Boolean) {
val currentSession = authenticationService.getLatestSessionId() sessionStore.getAllSessions()
if (currentSession != null) { .map { sessionData ->
matrixClientProvider.getOrRestore(currentSession) SessionId(sessionData.userId)
.getOrThrow() }
.logout(userInitiated = true, ignoreSdkError = true) .forEach { sessionId ->
} else { Timber.d("Logging out sessionId: $sessionId")
error("No session to sign out") matrixClientProvider.getOrRestore(sessionId).fold(
} onSuccess = { client ->
client.logout(userInitiated = true, ignoreSdkError = ignoreSdkError)
},
onFailure = { error ->
Timber.e(error, "Failed to get or restore MatrixClient for sessionId: $sessionId")
}
)
}
} }
} }

View file

@ -0,0 +1,120 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
@file:OptIn(ExperimentalCoroutinesApi::class)
package io.element.android.features.logout.impl
import io.element.android.libraries.matrix.test.A_USER_ID
import io.element.android.libraries.matrix.test.A_USER_ID_2
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.FakeMatrixClientProvider
import io.element.android.libraries.sessionstorage.test.InMemorySessionStore
import io.element.android.libraries.sessionstorage.test.aSessionData
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.Test
class DefaultLogoutUseCaseTest {
@Test
fun `test logout from one session`() = runTest {
val logoutLambda1 = lambdaRecorder<Boolean, Boolean, Unit> { _, _ -> }
val client1 = FakeMatrixClient(A_USER_ID).apply {
logoutLambda = logoutLambda1
}
val sut = DefaultLogoutUseCase(
sessionStore = InMemorySessionStore(
initialList = listOf(
aSessionData(sessionId = A_USER_ID.value),
)
),
matrixClientProvider = FakeMatrixClientProvider(
getClient = { sessionId ->
when (sessionId) {
A_USER_ID -> Result.success(client1)
else -> error("Unexpected sessionId")
}
}
),
)
sut.logoutAll(ignoreSdkError = true)
logoutLambda1.assertions().isCalledOnce().with(value(true), value(true))
}
@Test
fun `test logout from several sessions`() = runTest {
val logoutLambda1 = lambdaRecorder<Boolean, Boolean, Unit> { _, _ -> }
val logoutLambda2 = lambdaRecorder<Boolean, Boolean, Unit> { _, _ -> }
val client1 = FakeMatrixClient(A_USER_ID).apply {
logoutLambda = logoutLambda1
}
val client2 = FakeMatrixClient(A_USER_ID_2).apply {
logoutLambda = logoutLambda2
}
val sut = DefaultLogoutUseCase(
sessionStore = InMemorySessionStore(
initialList = listOf(
aSessionData(sessionId = A_USER_ID.value),
aSessionData(sessionId = A_USER_ID_2.value),
)
),
matrixClientProvider = FakeMatrixClientProvider(
getClient = { sessionId ->
when (sessionId) {
A_USER_ID -> Result.success(client1)
A_USER_ID_2 -> Result.success(client2)
else -> error("Unexpected sessionId")
}
}
),
)
sut.logoutAll(ignoreSdkError = true)
logoutLambda1.assertions().isCalledOnce().with(value(true), value(true))
logoutLambda2.assertions().isCalledOnce().with(value(true), value(true))
}
@Test
fun `test logout session not found is ignored`() = runTest {
val sut = DefaultLogoutUseCase(
sessionStore = InMemorySessionStore(
initialList = listOf(
aSessionData(sessionId = A_USER_ID.value),
)
),
matrixClientProvider = FakeMatrixClientProvider(
getClient = { sessionId ->
when (sessionId) {
A_USER_ID -> Result.failure(Exception("Session not found"))
else -> error("Unexpected sessionId")
}
}
),
)
sut.logoutAll(ignoreSdkError = true)
// No error
}
@Test
fun `test logout no sessions`() = runTest {
val sut = DefaultLogoutUseCase(
sessionStore = InMemorySessionStore(
initialList = emptyList()
),
matrixClientProvider = FakeMatrixClientProvider(
getClient = { sessionId ->
when (sessionId) {
else -> error("Unexpected sessionId")
}
}
),
)
sut.logoutAll(ignoreSdkError = true)
// No error
}
}

View file

@ -14,7 +14,7 @@ import io.element.android.tests.testutils.simulateLongTask
class FakeLogoutUseCase( class FakeLogoutUseCase(
var logoutLambda: (Boolean) -> Unit = { lambdaError() } var logoutLambda: (Boolean) -> Unit = { lambdaError() }
) : LogoutUseCase { ) : LogoutUseCase {
override suspend fun logout(ignoreSdkError: Boolean) = simulateLongTask { override suspend fun logoutAll(ignoreSdkError: Boolean) = simulateLongTask {
logoutLambda(ignoreSdkError) logoutLambda(ignoreSdkError)
} }
} }