diff --git a/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt index 94f344be7e..89800b1c82 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt @@ -54,6 +54,7 @@ import io.element.android.libraries.designsystem.theme.components.CircularProgre import io.element.android.libraries.di.AppScope 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.LoggedInState import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach @@ -97,15 +98,21 @@ class RootFlowNode @AssistedInject constructor( .distinctUntilChanged() .onEach { navState -> Timber.v("navState=$navState") - if (navState.isLoggedIn) { - tryToRestoreLatestSession( - onSuccess = { sessionId -> switchToLoggedInFlow(sessionId, navState.cacheIndex) }, - onFailure = { switchToNotLoggedInFlow() } - ) - } else { + when(navState.loggedInState) { + is LoggedInState.LoggedIn -> { + if(navState.loggedInState.isTokenValid) { + tryToRestoreLatestSession( + onSuccess = { sessionId -> switchToLoggedInFlow(sessionId, navState.cacheIndex) }, + onFailure = { switchToNotLoggedInFlow() } + ) + } else { + switchToSignedOutFlow() + } + } + LoggedInState.NotLoggedIn -> { switchToNotLoggedInFlow() + } } - } .launchIn(lifecycleScope) } @@ -118,6 +125,10 @@ class RootFlowNode @AssistedInject constructor( backstack.safeRoot(NavTarget.NotLoggedInFlow) } + private fun switchToSignedOutFlow() { + backstack.safeRoot(NavTarget.SignedOutFlow) + } + private suspend fun restoreSessionIfNeeded( sessionId: SessionId, onFailure: () -> Unit = {}, @@ -179,6 +190,9 @@ class RootFlowNode @AssistedInject constructor( val navId: Int ) : NavTarget + @Parcelize + data object SignedOutFlow : NavTarget + @Parcelize data object BugReport : NavTarget } @@ -198,6 +212,7 @@ class RootFlowNode @AssistedInject constructor( createNode(buildContext, plugins = listOf(inputs, callback)) } NavTarget.NotLoggedInFlow -> createNode(buildContext) + NavTarget.SignedOutFlow -> createNode(buildContext) NavTarget.SplashScreen -> splashNode(buildContext) NavTarget.BugReport -> { val callback = object : BugReportEntryPoint.Callback { diff --git a/appnav/src/main/kotlin/io/element/android/appnav/root/RootNavState.kt b/appnav/src/main/kotlin/io/element/android/appnav/root/RootNavState.kt index ed3ac15972..09f9767f62 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/root/RootNavState.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/root/RootNavState.kt @@ -16,6 +16,8 @@ package io.element.android.appnav.root +import io.element.android.libraries.sessionstorage.api.LoggedInState + /** * [RootNavState] produced by [RootNavStateFlowFactory]. */ @@ -26,7 +28,7 @@ data class RootNavState( */ val cacheIndex: Int, /** - * true if we are currently loggedIn. + * LoggedInState. */ - val isLoggedIn: Boolean + val loggedInState: LoggedInState, ) diff --git a/appnav/src/main/kotlin/io/element/android/appnav/root/RootNavStateFlowFactory.kt b/appnav/src/main/kotlin/io/element/android/appnav/root/RootNavStateFlowFactory.kt index 0e8d93b0c9..e6a46f5950 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/root/RootNavStateFlowFactory.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/root/RootNavStateFlowFactory.kt @@ -22,9 +22,9 @@ import io.element.android.appnav.di.MatrixClientsHolder import io.element.android.features.login.api.LoginUserStory import io.element.android.features.preferences.api.CacheService import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService +import io.element.android.libraries.sessionstorage.api.LoggedInState import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.onEach import javax.inject.Inject @@ -47,9 +47,14 @@ class RootNavStateFlowFactory @Inject constructor( fun create(savedStateMap: SavedStateMap?): Flow { return combine( cacheIndexFlow(savedStateMap), - isUserLoggedInFlow(), - ) { cacheIndex, isLoggedIn -> - RootNavState(cacheIndex = cacheIndex, isLoggedIn = isLoggedIn) + authenticationService.loggedInStateFlow(), + loginUserStory.loginFlowIsDone, + ) { cacheIndex, loggedInState, loginFlowIsDone -> + if (loginFlowIsDone) { + RootNavState(cacheIndex = cacheIndex, loggedInState = loggedInState) + } else { + RootNavState(cacheIndex = cacheIndex, loggedInState = LoggedInState.NotLoggedIn) + } } } @@ -72,16 +77,6 @@ class RootNavStateFlowFactory @Inject constructor( } } - private fun isUserLoggedInFlow(): Flow { - return combine( - authenticationService.isLoggedIn(), - loginUserStory.loginFlowIsDone - ) { isLoggedIn, loginFlowIsDone -> - isLoggedIn && loginFlowIsDone - } - .distinctUntilChanged() - } - /** * @return a flow of integer that increments the value by one each time a new element is emitted upstream. */ diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/MatrixAuthenticationService.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/MatrixAuthenticationService.kt index c15153876c..981ee028b2 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/MatrixAuthenticationService.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/MatrixAuthenticationService.kt @@ -18,11 +18,12 @@ package io.element.android.libraries.matrix.api.auth import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.sessionstorage.api.LoggedInState import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow interface MatrixAuthenticationService { - fun isLoggedIn(): Flow + fun loggedInStateFlow(): Flow suspend fun getLatestSessionId(): SessionId? suspend fun restoreSession(sessionId: SessionId): Result fun getHomeserverDetails(): StateFlow diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt index 6906953232..8b857d8076 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt @@ -126,7 +126,16 @@ class RustMatrixClient constructor( Timber.v("didReceiveAuthError -> do the cleanup") //TODO handle isSoftLogout parameter. appCoroutineScope.launch { - doLogout(doRequest = false) + val existingData = sessionStore.getSession(client.userId()) + if (existingData != null) { + // Set isTokenValid to false + val newData = client.session().toSessionData( + isTokenValid = false, + loginType = existingData.loginType, + ) + sessionStore.updateData(newData) + } + doLogout(doRequest = false, removeSession = false) } } else { Timber.v("didReceiveAuthError -> already cleaning up") @@ -333,9 +342,9 @@ class RustMatrixClient constructor( baseDirectory.deleteSessionDirectory(userID = sessionId.value, deleteCryptoDb = false) } - override suspend fun logout(): String? = doLogout(doRequest = true) + override suspend fun logout(): String? = doLogout(doRequest = true, removeSession = true) - private suspend fun doLogout(doRequest: Boolean): String? { + private suspend fun doLogout(doRequest: Boolean, removeSession: Boolean): String? { var result: String? = null withContext(sessionDispatcher) { if (doRequest) { @@ -347,7 +356,9 @@ class RustMatrixClient constructor( } close() baseDirectory.deleteSessionDirectory(userID = sessionId.value, deleteCryptoDb = true) - sessionStore.removeSession(sessionId.value) + if (removeSession) { + sessionStore.removeSession(sessionId.value) + } } return result } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationService.kt index af0c335dda..a0d924e945 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationService.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationService.kt @@ -30,6 +30,7 @@ import io.element.android.libraries.matrix.impl.RustMatrixClientFactory import io.element.android.libraries.matrix.impl.exception.mapClientException import io.element.android.libraries.matrix.impl.mapper.toSessionData import io.element.android.libraries.network.useragent.UserAgentProvider +import io.element.android.libraries.sessionstorage.api.LoggedInState import io.element.android.libraries.sessionstorage.api.LoginType import io.element.android.libraries.sessionstorage.api.SessionStore import kotlinx.coroutines.flow.Flow @@ -63,7 +64,7 @@ class RustMatrixAuthenticationService @Inject constructor( ) private var currentHomeserver = MutableStateFlow(null) - override fun isLoggedIn(): Flow { + override fun loggedInStateFlow(): Flow { return sessionStore.isLoggedIn() } diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/auth/FakeAuthenticationService.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/auth/FakeAuthenticationService.kt index 3a9a7fb2b9..2cf6b77a78 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/auth/FakeAuthenticationService.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/auth/FakeAuthenticationService.kt @@ -22,6 +22,7 @@ import io.element.android.libraries.matrix.api.auth.MatrixHomeServerDetails import io.element.android.libraries.matrix.api.auth.OidcDetails import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.test.A_USER_ID +import io.element.android.libraries.sessionstorage.api.LoggedInState import io.element.android.tests.testutils.simulateLongTask import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow @@ -38,8 +39,8 @@ class FakeAuthenticationService : MatrixAuthenticationService { private var changeServerError: Throwable? = null private var matrixClient: MatrixClient? = null - override fun isLoggedIn(): Flow { - return flowOf(false) + override fun loggedInStateFlow(): Flow { + return flowOf(LoggedInState.NotLoggedIn) } override suspend fun getLatestSessionId(): SessionId? { diff --git a/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/LoggedInState.kt b/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/LoggedInState.kt new file mode 100644 index 0000000000..5cee58a392 --- /dev/null +++ b/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/LoggedInState.kt @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.sessionstorage.api + +sealed interface LoggedInState { + data object NotLoggedIn : LoggedInState + data class LoggedIn(val isTokenValid: Boolean) : LoggedInState +} diff --git a/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/SessionStore.kt b/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/SessionStore.kt index 2b3398f76c..cc23353a8f 100644 --- a/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/SessionStore.kt +++ b/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/SessionStore.kt @@ -20,7 +20,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map interface SessionStore { - fun isLoggedIn(): Flow + fun isLoggedIn(): Flow fun sessionsFlow(): Flow> suspend fun storeData(sessionData: SessionData) diff --git a/libraries/session-storage/impl-memory/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/memory/InMemorySessionStore.kt b/libraries/session-storage/impl-memory/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/memory/InMemorySessionStore.kt index df78149eef..abbf65a3bd 100644 --- a/libraries/session-storage/impl-memory/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/memory/InMemorySessionStore.kt +++ b/libraries/session-storage/impl-memory/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/memory/InMemorySessionStore.kt @@ -16,6 +16,7 @@ package io.element.android.libraries.sessionstorage.impl.memory +import io.element.android.libraries.sessionstorage.api.LoggedInState import io.element.android.libraries.sessionstorage.api.SessionData import io.element.android.libraries.sessionstorage.api.SessionStore import kotlinx.coroutines.flow.Flow @@ -26,8 +27,14 @@ class InMemorySessionStore : SessionStore { private var sessionDataFlow = MutableStateFlow(null) - override fun isLoggedIn(): Flow { - return sessionDataFlow.map { it != null } + override fun isLoggedIn(): Flow { + return sessionDataFlow.map { + if (it == null) { + LoggedInState.NotLoggedIn + } else { + LoggedInState.LoggedIn(it.isTokenValid) + } + } } override fun sessionsFlow(): Flow> { diff --git a/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/DatabaseSessionStore.kt b/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/DatabaseSessionStore.kt index eb273411a0..4d6b035ee4 100644 --- a/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/DatabaseSessionStore.kt +++ b/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/DatabaseSessionStore.kt @@ -22,6 +22,7 @@ import com.squareup.sqldelight.runtime.coroutines.mapToList import com.squareup.sqldelight.runtime.coroutines.mapToOneOrNull import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.SingleIn +import io.element.android.libraries.sessionstorage.api.LoggedInState import io.element.android.libraries.sessionstorage.api.SessionData import io.element.android.libraries.sessionstorage.api.SessionStore import kotlinx.coroutines.flow.Flow @@ -35,11 +36,17 @@ class DatabaseSessionStore @Inject constructor( private val database: SessionDatabase, ) : SessionStore { - override fun isLoggedIn(): Flow { + override fun isLoggedIn(): Flow { return database.sessionDataQueries.selectFirst() .asFlow() .mapToOneOrNull() - .map { it != null } + .map { + if (it == null) { + LoggedInState.NotLoggedIn + } else { + LoggedInState.LoggedIn((it.isTokenValid ?: 1) == 1L) + } + } } override suspend fun storeData(sessionData: SessionData) { diff --git a/libraries/session-storage/impl/src/test/kotlin/io/element/android/libraries/sessionstorage/impl/DatabaseSessionStoreTests.kt b/libraries/session-storage/impl/src/test/kotlin/io/element/android/libraries/sessionstorage/impl/DatabaseSessionStoreTests.kt index b24543e9d9..5126082b6f 100644 --- a/libraries/session-storage/impl/src/test/kotlin/io/element/android/libraries/sessionstorage/impl/DatabaseSessionStoreTests.kt +++ b/libraries/session-storage/impl/src/test/kotlin/io/element/android/libraries/sessionstorage/impl/DatabaseSessionStoreTests.kt @@ -20,6 +20,7 @@ import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import com.squareup.sqldelight.sqlite.driver.JdbcSqliteDriver import io.element.android.libraries.matrix.session.SessionData +import io.element.android.libraries.sessionstorage.api.LoggedInState import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test @@ -65,11 +66,11 @@ class DatabaseSessionStoreTests { @Test fun `isLoggedIn emits true while there are sessions in the DB`() = runTest { databaseSessionStore.isLoggedIn().test { - assertThat(awaitItem()).isFalse() + assertThat(awaitItem()).isEqualTo(LoggedInState.NotLoggedIn) database.sessionDataQueries.insertSessionData(aSessionData) - assertThat(awaitItem()).isTrue() + assertThat(awaitItem()).isEqualTo(LoggedInState.LoggedIn(true)) database.sessionDataQueries.removeSession(aSessionData.userId) - assertThat(awaitItem()).isFalse() + assertThat(awaitItem()).isEqualTo(LoggedInState.NotLoggedIn) } } diff --git a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/MainActivity.kt b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/MainActivity.kt index d0309ebec7..035d5a3e3b 100644 --- a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/MainActivity.kt +++ b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/MainActivity.kt @@ -30,6 +30,7 @@ import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService import io.element.android.libraries.matrix.impl.RustMatrixClientFactory import io.element.android.libraries.matrix.impl.auth.RustMatrixAuthenticationService import io.element.android.libraries.network.useragent.SimpleUserAgentProvider +import io.element.android.libraries.sessionstorage.api.LoggedInState import io.element.android.libraries.sessionstorage.impl.memory.InMemorySessionStore import io.element.android.libraries.theme.ElementTheme import io.element.android.services.toolbox.impl.systemclock.DefaultSystemClock @@ -64,8 +65,8 @@ class MainActivity : ComponentActivity() { WindowCompat.setDecorFitsSystemWindows(window, false) setContent { ElementTheme { - val isLoggedIn by matrixAuthenticationService.isLoggedIn().collectAsState(initial = false) - Content(isLoggedIn = isLoggedIn, modifier = Modifier.fillMaxSize()) + val isLoggedIn by matrixAuthenticationService.isLoggedIn().collectAsState(initial = LoggedInState.NotLoggedIn) + Content(isLoggedIn = isLoggedIn is LoggedInState.LoggedIn, modifier = Modifier.fillMaxSize()) } }