From 904d45b255633c3c94d694b34905f7d2cfd5a653 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 9 Oct 2023 14:53:41 +0200 Subject: [PATCH 01/18] Use the ktx version of androidx.sqlite:sqlite --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a5ce502f9c..89a511fc99 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -157,7 +157,7 @@ sqldelight-driver-android = { module = "com.squareup.sqldelight:android-driver", sqldelight-driver-jvm = { module = "com.squareup.sqldelight:sqlite-driver", version.ref = "sqldelight" } sqldelight-coroutines = { module = "com.squareup.sqldelight:coroutines-extensions", version.ref = "sqldelight" } sqlcipher = "net.zetetic:android-database-sqlcipher:4.5.4" -sqlite = "androidx.sqlite:sqlite:2.3.1" +sqlite = "androidx.sqlite:sqlite-ktx:2.3.1" unifiedpush = "com.github.UnifiedPush:android-connector:2.1.1" otaliastudios_transcoder = "com.otaliastudios:transcoder:0.10.5" vanniktech_blurhash = "com.vanniktech:blurhash:0.1.0" From a1afdeb6d3bbadf34a1076eb506da4ac279ac3d2 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 9 Oct 2023 15:23:16 +0200 Subject: [PATCH 02/18] Generate database with model version 3, to unit test database migration. https://cashapp.github.io/sqldelight/1.5.4/multiplatform_sqlite/migrations/ --- libraries/session-storage/impl/build.gradle.kts | 6 ++++++ .../impl/src/main/sqldelight/databases/3.db | Bin 0 -> 12288 bytes 2 files changed, 6 insertions(+) create mode 100644 libraries/session-storage/impl/src/main/sqldelight/databases/3.db diff --git a/libraries/session-storage/impl/build.gradle.kts b/libraries/session-storage/impl/build.gradle.kts index 698bfcf230..51d02e4e6a 100644 --- a/libraries/session-storage/impl/build.gradle.kts +++ b/libraries/session-storage/impl/build.gradle.kts @@ -49,6 +49,12 @@ dependencies { sqldelight { database("SessionDatabase") { + // https://cashapp.github.io/sqldelight/1.5.4/multiplatform_sqlite/migrations/ + // To generate a .db file from your latest schema, run this task + // ./gradlew generateDebugSessionDatabaseSchema + // Test migration by running + // ./gradlew verifySqlDelightMigration + schemaOutputDirectory = File("src/main/sqldelight/databases") verifyMigrations = true } } diff --git a/libraries/session-storage/impl/src/main/sqldelight/databases/3.db b/libraries/session-storage/impl/src/main/sqldelight/databases/3.db new file mode 100644 index 0000000000000000000000000000000000000000..58c8e8f09a089910fc615d10e7f1caf66d5ee069 GIT binary patch literal 12288 zcmeI#O-sWt7zglliZ6q`+;)xZq9B5Zcc&I9t8=y;xYMX@wgtOJ({5~6@dNk;_F?>1 z9!*+?RfOH=KajRhpC<|VwU@Z}HcuHH3OVspdPeHRFvta^gb+jZE!9^|$J?tw-F3XK z8RVq>Ua5VOQgunH%i71f0mwrD0uX=z1Rwwb2tWV=5P-lx6nH%_b{oy6@wT7(j}dcO zl5i1U`>9`eEL}Nf%QLChy0lGNNYKMFRc)FuX@%4?A3WN*Q@v-~wCh;6EvHZ0X8$;= z3fYVY>~~$B^MgRG?1?89|C}aIOE#1&8Gq;V*JCkZYUvr1JsJHqE{S-^#Q-2tWV=5P$##AOHafKmY;|*i?bC-WbmRoBDaNF9<*Y V0uX=z1Rwwb2tWV=5P(1?@C8$tbcz4~ literal 0 HcmV?d00001 From a424bc26edb40b3aa4010fead217d8fd5023c896 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 9 Oct 2023 15:40:42 +0200 Subject: [PATCH 03/18] Need this now. --- libraries/session-storage/impl/build.gradle.kts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/libraries/session-storage/impl/build.gradle.kts b/libraries/session-storage/impl/build.gradle.kts index 51d02e4e6a..4e76c7de9c 100644 --- a/libraries/session-storage/impl/build.gradle.kts +++ b/libraries/session-storage/impl/build.gradle.kts @@ -45,6 +45,8 @@ dependencies { testImplementation(libs.test.turbine) testImplementation(libs.coroutines.test) testImplementation(libs.sqldelight.driver.jvm) + + coreLibraryDesugaring(libs.android.desugar) } sqldelight { From 8305912b14e9747f07e26409f5450c4b43732268 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 9 Oct 2023 15:59:28 +0200 Subject: [PATCH 04/18] Map the new fields of `SessionData`. --- .../libraries/matrix/impl/RustMatrixClient.kt | 7 ++- .../auth/RustMatrixAuthenticationService.kt | 15 ++++++- .../libraries/matrix/impl/mapper/Session.kt | 8 +++- .../libraries/sessionstorage/api/LoginType.kt | 43 +++++++++++++++++++ .../sessionstorage/api/SessionData.kt | 2 + .../sessionstorage/impl/SessionDataMapper.kt | 7 ++- .../impl/DatabaseSessionStoreTests.kt | 6 +++ 7 files changed, 83 insertions(+), 5 deletions(-) create mode 100644 libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/LoginType.kt 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 ba20cbea2d..6906953232 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 @@ -136,7 +136,12 @@ class RustMatrixClient constructor( override fun didRefreshTokens() { Timber.w("didRefreshTokens()") appCoroutineScope.launch { - sessionStore.updateData(client.session().toSessionData()) + val existingData = sessionStore.getSession(client.userId()) ?: return@launch + val newData = client.session().toSessionData( + isTokenValid = existingData.isTokenValid, + loginType = existingData.loginType, + ) + sessionStore.updateData(newData) } } } 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 f2acb0b1be..af0c335dda 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.LoginType import io.element.android.libraries.sessionstorage.api.SessionStore import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow @@ -102,7 +103,12 @@ class RustMatrixAuthenticationService @Inject constructor( withContext(coroutineDispatchers.io) { runCatching { val client = authService.login(username, password, "Element X Android", null) - val sessionData = client.use { it.session().toSessionData() } + val sessionData = client.use { + it.session().toSessionData( + isTokenValid = true, + loginType = LoginType.PASSWORD, + ) + } sessionStore.storeData(sessionData) SessionId(sessionData.userId) }.mapFailure { failure -> @@ -144,7 +150,12 @@ class RustMatrixAuthenticationService @Inject constructor( runCatching { val urlForOidcLogin = pendingOidcAuthenticationData ?: error("You need to call `getOidcUrl()` first") val client = authService.loginWithOidcCallback(urlForOidcLogin, callbackUrl) - val sessionData = client.use { it.session().toSessionData() } + val sessionData = client.use { + it.session().toSessionData( + isTokenValid = true, + loginType = LoginType.OIDC, + ) + } pendingOidcAuthenticationData?.close() pendingOidcAuthenticationData = null sessionStore.storeData(sessionData) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/mapper/Session.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/mapper/Session.kt index 825c6f4397..fe21a460c8 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/mapper/Session.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/mapper/Session.kt @@ -16,11 +16,15 @@ package io.element.android.libraries.matrix.impl.mapper +import io.element.android.libraries.sessionstorage.api.LoginType import io.element.android.libraries.sessionstorage.api.SessionData import org.matrix.rustcomponents.sdk.Session import java.util.Date -internal fun Session.toSessionData() = SessionData( +internal fun Session.toSessionData( + isTokenValid: Boolean, + loginType: LoginType, +) = SessionData( userId = userId, deviceId = deviceId, accessToken = accessToken, @@ -29,4 +33,6 @@ internal fun Session.toSessionData() = SessionData( oidcData = oidcData, slidingSyncProxy = slidingSyncProxy, loginTimestamp = Date(), + isTokenValid = isTokenValid, + loginType = loginType, ) diff --git a/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/LoginType.kt b/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/LoginType.kt new file mode 100644 index 0000000000..a1e4400797 --- /dev/null +++ b/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/LoginType.kt @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. + * + * 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 + +// Imported from Element Android, to be able to migrate from EA to EXA. +enum class LoginType { + PASSWORD, + OIDC, + SSO, + UNSUPPORTED, + CUSTOM, + DIRECT, + UNKNOWN, + QR; + + companion object { + + fun fromName(name: String) = when (name) { + PASSWORD.name -> PASSWORD + OIDC.name -> OIDC + SSO.name -> SSO + UNSUPPORTED.name -> UNSUPPORTED + CUSTOM.name -> CUSTOM + DIRECT.name -> DIRECT + QR.name -> QR + else -> UNKNOWN + } + } +} diff --git a/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/SessionData.kt b/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/SessionData.kt index e14c3feeab..25a48c0efe 100644 --- a/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/SessionData.kt +++ b/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/SessionData.kt @@ -27,4 +27,6 @@ data class SessionData( val oidcData: String?, val slidingSyncProxy: String?, val loginTimestamp: Date?, + val isTokenValid: Boolean, + val loginType: LoginType, ) diff --git a/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/SessionDataMapper.kt b/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/SessionDataMapper.kt index d0c89d9896..7c00d98f91 100644 --- a/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/SessionDataMapper.kt +++ b/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/SessionDataMapper.kt @@ -16,6 +16,7 @@ package io.element.android.libraries.sessionstorage.impl +import io.element.android.libraries.sessionstorage.api.LoginType import io.element.android.libraries.sessionstorage.api.SessionData import java.util.Date import io.element.android.libraries.matrix.session.SessionData as DbSessionData @@ -30,6 +31,8 @@ internal fun SessionData.toDbModel(): DbSessionData { oidcData = oidcData, slidingSyncProxy = slidingSyncProxy, loginTimestamp = loginTimestamp?.time, + isTokenValid = if (isTokenValid) 1L else 0L, + loginType = loginType.name, ) } @@ -42,6 +45,8 @@ internal fun DbSessionData.toApiModel(): SessionData { homeserverUrl = homeserverUrl, oidcData = oidcData, slidingSyncProxy = slidingSyncProxy, - loginTimestamp = loginTimestamp?.let { Date(it) } + loginTimestamp = loginTimestamp?.let { Date(it) }, + isTokenValid = (isTokenValid ?: 1) == 1L, + loginType = LoginType.fromName(loginType ?: LoginType.UNKNOWN.name), ) } 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 e035ff9ae1..b24543e9d9 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 @@ -38,6 +38,8 @@ class DatabaseSessionStoreTests { slidingSyncProxy = null, loginTimestamp = null, oidcData = "aOidcData", + isTokenValid = null, + loginType = null, ) @Before @@ -121,6 +123,8 @@ class DatabaseSessionStoreTests { slidingSyncProxy = "slidingSyncProxy", loginTimestamp = 1, oidcData = "aOidcData", + isTokenValid = null, + loginType = null, ) val secondSessionData = SessionData( userId = "userId", @@ -131,6 +135,8 @@ class DatabaseSessionStoreTests { slidingSyncProxy = "slidingSyncProxyAltered", loginTimestamp = 2, oidcData = "aOidcDataAltered", + isTokenValid = null, + loginType = null, ) assertThat(firstSessionData.userId).isEqualTo(secondSessionData.userId) assertThat(firstSessionData.loginTimestamp).isNotEqualTo(secondSessionData.loginTimestamp) From 124d6bf95f23a2c184ac368d9fbc2f11b1f961c1 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 9 Oct 2023 17:38:28 +0200 Subject: [PATCH 05/18] SignedOut mode - WIP --- .../io/element/android/appnav/RootFlowNode.kt | 29 ++++++++++++++----- .../android/appnav/root/RootNavState.kt | 6 ++-- .../appnav/root/RootNavStateFlowFactory.kt | 23 ++++++--------- .../api/auth/MatrixAuthenticationService.kt | 3 +- .../libraries/matrix/impl/RustMatrixClient.kt | 19 +++++++++--- .../auth/RustMatrixAuthenticationService.kt | 3 +- .../test/auth/FakeAuthenticationService.kt | 5 ++-- .../sessionstorage/api/LoggedInState.kt | 22 ++++++++++++++ .../sessionstorage/api/SessionStore.kt | 2 +- .../impl/memory/InMemorySessionStore.kt | 11 +++++-- .../impl/DatabaseSessionStore.kt | 11 +++++-- .../impl/DatabaseSessionStoreTests.kt | 7 +++-- .../android/samples/minimal/MainActivity.kt | 5 ++-- 13 files changed, 105 insertions(+), 41 deletions(-) create mode 100644 libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/LoggedInState.kt 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()) } } From 266f93cc28221feef98326b95b13d4d71aa5012c Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 9 Oct 2023 19:46:42 +0200 Subject: [PATCH 06/18] Add SignedOutNode, to handle session behind deleted from outside (no support for soft-logout) --- appnav/build.gradle.kts | 1 + .../io/element/android/appnav/RootFlowNode.kt | 25 +-- .../appnav/signedout/SignedOutEvents.kt | 21 +++ .../android/appnav/signedout/SignedOutNode.kt | 54 +++++++ .../appnav/signedout/SignedOutPresenter.kt | 63 ++++++++ .../appnav/signedout/SignedOutState.kt | 25 +++ .../signedout/SignedOutStateProvider.kt | 53 ++++++ .../android/appnav/signedout/SignedOutView.kt | 153 ++++++++++++++++++ .../signedout/SignedOutPresenterTest.kt | 81 ++++++++++ .../sessionstorage/api/LoggedInState.kt | 5 +- .../impl/memory/InMemorySessionStore.kt | 5 +- .../impl/DatabaseSessionStore.kt | 5 +- 12 files changed, 479 insertions(+), 12 deletions(-) create mode 100644 appnav/src/main/kotlin/io/element/android/appnav/signedout/SignedOutEvents.kt create mode 100644 appnav/src/main/kotlin/io/element/android/appnav/signedout/SignedOutNode.kt create mode 100644 appnav/src/main/kotlin/io/element/android/appnav/signedout/SignedOutPresenter.kt create mode 100644 appnav/src/main/kotlin/io/element/android/appnav/signedout/SignedOutState.kt create mode 100644 appnav/src/main/kotlin/io/element/android/appnav/signedout/SignedOutStateProvider.kt create mode 100644 appnav/src/main/kotlin/io/element/android/appnav/signedout/SignedOutView.kt create mode 100644 appnav/src/test/kotlin/io/element/android/appnav/signedout/SignedOutPresenterTest.kt diff --git a/appnav/build.gradle.kts b/appnav/build.gradle.kts index 88f0741ebe..f7d8540c4e 100644 --- a/appnav/build.gradle.kts +++ b/appnav/build.gradle.kts @@ -64,6 +64,7 @@ dependencies { testImplementation(libs.test.turbine) testImplementation(projects.libraries.matrix.test) testImplementation(projects.features.networkmonitor.test) + testImplementation(projects.libraries.sessionStorage.implMemory) testImplementation(projects.tests.testutils) testImplementation(projects.features.rageshake.test) testImplementation(projects.features.rageshake.impl) 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 89800b1c82..6be25d5bc2 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt @@ -42,6 +42,7 @@ import io.element.android.appnav.intent.ResolvedIntent import io.element.android.appnav.root.RootNavStateFlowFactory import io.element.android.appnav.root.RootPresenter import io.element.android.appnav.root.RootView +import io.element.android.appnav.signedout.SignedOutNode import io.element.android.features.login.api.oidc.OidcAction import io.element.android.features.login.api.oidc.OidcActionFlow import io.element.android.features.rageshake.api.bugreport.BugReportEntryPoint @@ -98,21 +99,22 @@ class RootFlowNode @AssistedInject constructor( .distinctUntilChanged() .onEach { navState -> Timber.v("navState=$navState") - when(navState.loggedInState) { + when (navState.loggedInState) { is LoggedInState.LoggedIn -> { - if(navState.loggedInState.isTokenValid) { + if (navState.loggedInState.isTokenValid) { tryToRestoreLatestSession( onSuccess = { sessionId -> switchToLoggedInFlow(sessionId, navState.cacheIndex) }, onFailure = { switchToNotLoggedInFlow() } ) } else { - switchToSignedOutFlow() + switchToSignedOutFlow(SessionId((navState.loggedInState.sessionId))) } } - LoggedInState.NotLoggedIn -> { - switchToNotLoggedInFlow() + LoggedInState.NotLoggedIn -> { + switchToNotLoggedInFlow() } } + } .launchIn(lifecycleScope) } @@ -125,8 +127,8 @@ class RootFlowNode @AssistedInject constructor( backstack.safeRoot(NavTarget.NotLoggedInFlow) } - private fun switchToSignedOutFlow() { - backstack.safeRoot(NavTarget.SignedOutFlow) + private fun switchToSignedOutFlow(sessionId: SessionId) { + backstack.safeRoot(NavTarget.SignedOutFlow(sessionId)) } private suspend fun restoreSessionIfNeeded( @@ -191,7 +193,9 @@ class RootFlowNode @AssistedInject constructor( ) : NavTarget @Parcelize - data object SignedOutFlow : NavTarget + data class SignedOutFlow( + val sessionId: SessionId + ) : NavTarget @Parcelize data object BugReport : NavTarget @@ -212,7 +216,10 @@ class RootFlowNode @AssistedInject constructor( createNode(buildContext, plugins = listOf(inputs, callback)) } NavTarget.NotLoggedInFlow -> createNode(buildContext) - NavTarget.SignedOutFlow -> createNode(buildContext) + is NavTarget.SignedOutFlow -> { + val inputs = SignedOutNode.Inputs(navTarget.sessionId) + createNode(buildContext, listOf(inputs)) + } NavTarget.SplashScreen -> splashNode(buildContext) NavTarget.BugReport -> { val callback = object : BugReportEntryPoint.Callback { diff --git a/appnav/src/main/kotlin/io/element/android/appnav/signedout/SignedOutEvents.kt b/appnav/src/main/kotlin/io/element/android/appnav/signedout/SignedOutEvents.kt new file mode 100644 index 0000000000..3c3c61696b --- /dev/null +++ b/appnav/src/main/kotlin/io/element/android/appnav/signedout/SignedOutEvents.kt @@ -0,0 +1,21 @@ +/* + * 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.appnav.signedout + +sealed interface SignedOutEvents { + data object SignInAgain : SignedOutEvents +} diff --git a/appnav/src/main/kotlin/io/element/android/appnav/signedout/SignedOutNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/signedout/SignedOutNode.kt new file mode 100644 index 0000000000..25917daab5 --- /dev/null +++ b/appnav/src/main/kotlin/io/element/android/appnav/signedout/SignedOutNode.kt @@ -0,0 +1,54 @@ +/* + * 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.appnav.signedout + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import io.element.android.anvilannotations.ContributesNode +import io.element.android.libraries.architecture.NodeInputs +import io.element.android.libraries.architecture.inputs +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.matrix.api.core.SessionId + +@ContributesNode(AppScope::class) +class SignedOutNode @AssistedInject constructor( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, + presenterFactory: SignedOutPresenter.Factory, +) : Node(buildContext, plugins = plugins) { + + data class Inputs( + val sessionId: SessionId, + ) : NodeInputs + + private val inputs: Inputs = inputs() + private val presenter = presenterFactory.create(inputs.sessionId.value) + + @Composable + override fun View(modifier: Modifier) { + val state = presenter.present() + SignedOutView( + state = state, + modifier = modifier + ) + } +} diff --git a/appnav/src/main/kotlin/io/element/android/appnav/signedout/SignedOutPresenter.kt b/appnav/src/main/kotlin/io/element/android/appnav/signedout/SignedOutPresenter.kt new file mode 100644 index 0000000000..1292ad0496 --- /dev/null +++ b/appnav/src/main/kotlin/io/element/android/appnav/signedout/SignedOutPresenter.kt @@ -0,0 +1,63 @@ +/* + * 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.appnav.signedout + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.sessionstorage.api.SessionStore +import kotlinx.coroutines.launch + +class SignedOutPresenter @AssistedInject constructor( + @Assisted private val sessionId: String, /* Cannot inject SessionId */ + private val sessionStore: SessionStore, +) : Presenter { + + @AssistedFactory + interface Factory { + fun create(sessionId: String): SignedOutPresenter + } + + @Composable + override fun present(): SignedOutState { + val sessions by sessionStore.sessionsFlow().collectAsState(initial = emptyList()) + val signedOutSession by remember { + derivedStateOf { sessions.firstOrNull { it.userId == sessionId } } + } + val coroutineScope = rememberCoroutineScope() + + fun handleEvents(event: SignedOutEvents) { + when (event) { + SignedOutEvents.SignInAgain -> coroutineScope.launch { + sessionStore.removeSession(sessionId) + } + } + } + + return SignedOutState( + signedOutSession = signedOutSession, + eventSink = ::handleEvents + ) + } +} diff --git a/appnav/src/main/kotlin/io/element/android/appnav/signedout/SignedOutState.kt b/appnav/src/main/kotlin/io/element/android/appnav/signedout/SignedOutState.kt new file mode 100644 index 0000000000..f181c537ba --- /dev/null +++ b/appnav/src/main/kotlin/io/element/android/appnav/signedout/SignedOutState.kt @@ -0,0 +1,25 @@ +/* + * 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.appnav.signedout + +import io.element.android.libraries.sessionstorage.api.SessionData + +// Do not use default value, so no member get forgotten in the presenters. +data class SignedOutState( + val signedOutSession: SessionData?, + val eventSink: (SignedOutEvents) -> Unit, +) diff --git a/appnav/src/main/kotlin/io/element/android/appnav/signedout/SignedOutStateProvider.kt b/appnav/src/main/kotlin/io/element/android/appnav/signedout/SignedOutStateProvider.kt new file mode 100644 index 0000000000..b7a81cf86b --- /dev/null +++ b/appnav/src/main/kotlin/io/element/android/appnav/signedout/SignedOutStateProvider.kt @@ -0,0 +1,53 @@ +/* + * 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.appnav.signedout + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.sessionstorage.api.LoginType +import io.element.android.libraries.sessionstorage.api.SessionData + +open class SignedOutStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + aSignedOutState(), + // Add other states here + ) +} + +fun aSignedOutState() = SignedOutState( + eventSink = {}, + signedOutSession = aSessionData() +) + +fun aSessionData( + sessionId: SessionId = SessionId("@alice:server.org"), + isTokenValid: Boolean = false, +): SessionData { + return SessionData( + userId = sessionId.value, + deviceId = "aDeviceId", + accessToken = "anAccessToken", + refreshToken = "aRefreshToken", + homeserverUrl = "aHomeserverUrl", + oidcData = null, + slidingSyncProxy = null, + loginTimestamp = null, + isTokenValid = isTokenValid, + loginType = LoginType.UNKNOWN, + ) +} diff --git a/appnav/src/main/kotlin/io/element/android/appnav/signedout/SignedOutView.kt b/appnav/src/main/kotlin/io/element/android/appnav/signedout/SignedOutView.kt new file mode 100644 index 0000000000..7c31e7713b --- /dev/null +++ b/appnav/src/main/kotlin/io/element/android/appnav/signedout/SignedOutView.kt @@ -0,0 +1,153 @@ +/* + * 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.appnav.signedout + +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.AccountCircle +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.BiasAlignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule +import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule +import io.element.android.libraries.designsystem.atomic.organisms.InfoListItem +import io.element.android.libraries.designsystem.atomic.organisms.InfoListOrganism +import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage +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 +import io.element.android.libraries.designsystem.theme.components.Icon +import io.element.android.libraries.designsystem.theme.temporaryColorBgSpecial +import io.element.android.libraries.designsystem.utils.CommonDrawables +import io.element.android.libraries.theme.ElementTheme +import kotlinx.collections.immutable.persistentListOf + +// TODO i18n, when wording has been approved. +@Composable +fun SignedOutView( + state: SignedOutState, + modifier: Modifier = Modifier, +) { + BackHandler(onBack = { state.eventSink(SignedOutEvents.SignInAgain) }) + HeaderFooterPage( + modifier = modifier + .fillMaxSize() + .systemBarsPadding() + .imePadding(), + header = { SignedOutHeader() }, + content = { SignedOutContent() }, + footer = { + SignedOutFooter( + onSignInAgain = { state.eventSink(SignedOutEvents.SignInAgain) }, + ) + } + ) +} + +@Composable +fun SignedOutHeader() { + IconTitleSubtitleMolecule( + modifier = Modifier.padding(top = 60.dp, bottom = 12.dp), + title = "You’re signed out", + subTitle = "It can be due to various reasons:", + iconImageVector = Icons.Filled.AccountCircle + ) +} + +@Composable +private fun SignedOutContent( + modifier: Modifier = Modifier, +) { + Box( + modifier = modifier.fillMaxSize(), + contentAlignment = BiasAlignment( + horizontalBias = 0f, + verticalBias = -0.4f + ) + ) { + InfoListOrganism( + items = persistentListOf( + InfoListItem( + message = "You’ve changed your password on another session.", + iconComposable = { CheckIcon() }, + ), + InfoListItem( + message = "You have deleted this session from another session.", + iconComposable = { CheckIcon() }, + ), + InfoListItem( + message = "The administrator of your server has invalidated your access for security reason.", + iconComposable = { CheckIcon() }, + ), + ), + textStyle = ElementTheme.typography.fontBodyMdMedium, + iconTint = ElementTheme.colors.textPrimary, + backgroundColor = ElementTheme.colors.temporaryColorBgSpecial + ) + } +} + +@Composable +private fun CheckIcon(modifier: Modifier = Modifier) { + Icon( + modifier = modifier + .size(20.dp) + .background(color = MaterialTheme.colorScheme.background, shape = CircleShape) + .padding(2.dp), + resourceId = CommonDrawables.ic_compound_check, + contentDescription = null, + tint = ElementTheme.colors.textActionAccent, + ) +} + +@Composable +private fun SignedOutFooter( + modifier: Modifier = Modifier, + onSignInAgain: () -> Unit, +) { + ButtonColumnMolecule( + modifier = modifier, + ) { + Button( + text = "Sign in again", + onClick = onSignInAgain, + modifier = Modifier.fillMaxWidth(), + ) + } +} + +@PreviewsDayNight +@Composable +fun SignedOutViewPreview( + @PreviewParameter(SignedOutStateProvider::class) state: SignedOutState, +) = ElementPreview { + SignedOutView( + state = state, + ) +} diff --git a/appnav/src/test/kotlin/io/element/android/appnav/signedout/SignedOutPresenterTest.kt b/appnav/src/test/kotlin/io/element/android/appnav/signedout/SignedOutPresenterTest.kt new file mode 100644 index 0000000000..529123250c --- /dev/null +++ b/appnav/src/test/kotlin/io/element/android/appnav/signedout/SignedOutPresenterTest.kt @@ -0,0 +1,81 @@ +/* + * 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.appnav.signedout + +import app.cash.molecule.RecompositionMode +import app.cash.molecule.moleculeFlow +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.matrix.test.A_SESSION_ID +import io.element.android.libraries.sessionstorage.api.SessionStore +import io.element.android.libraries.sessionstorage.impl.memory.InMemorySessionStore +import io.element.android.tests.testutils.WarmUpRule +import kotlinx.coroutines.test.runTest +import org.junit.Rule +import org.junit.Test + +class SignedOutPresenterTest { + @get:Rule + val warmUpRule = WarmUpRule() + + @Test + fun `present - initial state`() = runTest { + val aSessionData = aSessionData() + val sessionStore = InMemorySessionStore().apply { + storeData(aSessionData) + } + val presenter = createPresenter(sessionStore = sessionStore) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + skipItems(1) + val initialState = awaitItem() + assertThat(initialState.signedOutSession).isEqualTo(aSessionData) + } + } + + @Test + fun `present - sign in again`() = runTest { + val aSessionData = aSessionData() + val sessionStore = InMemorySessionStore().apply { + storeData(aSessionData) + } + val presenter = createPresenter(sessionStore = sessionStore) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + skipItems(1) + val initialState = awaitItem() + assertThat(initialState.signedOutSession).isEqualTo(aSessionData) + assertThat(sessionStore.getAllSessions()).isNotEmpty() + initialState.eventSink(SignedOutEvents.SignInAgain) + assertThat(awaitItem().signedOutSession).isNull() + assertThat(sessionStore.getAllSessions()).isEmpty() + } + } + + private fun createPresenter( + sessionId: SessionId = A_SESSION_ID, + sessionStore: SessionStore = InMemorySessionStore(), + ): SignedOutPresenter { + return SignedOutPresenter( + sessionId = sessionId.value, + sessionStore = sessionStore, + ) + } +} 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 index 5cee58a392..ddf7ae5e8a 100644 --- 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 @@ -18,5 +18,8 @@ package io.element.android.libraries.sessionstorage.api sealed interface LoggedInState { data object NotLoggedIn : LoggedInState - data class LoggedIn(val isTokenValid: Boolean) : LoggedInState + data class LoggedIn( + val sessionId: String, + val isTokenValid: Boolean, + ) : LoggedInState } 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 abbf65a3bd..4b76e82e8b 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 @@ -32,7 +32,10 @@ class InMemorySessionStore : SessionStore { if (it == null) { LoggedInState.NotLoggedIn } else { - LoggedInState.LoggedIn(it.isTokenValid) + LoggedInState.LoggedIn( + sessionId = it.userId, + isTokenValid = it.isTokenValid, + ) } } } 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 4d6b035ee4..1592e7646e 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 @@ -44,7 +44,10 @@ class DatabaseSessionStore @Inject constructor( if (it == null) { LoggedInState.NotLoggedIn } else { - LoggedInState.LoggedIn((it.isTokenValid ?: 1) == 1L) + LoggedInState.LoggedIn( + sessionId = it.userId, + isTokenValid = (it.isTokenValid ?: 1) == 1L + ) } } } From 6be984efc8af2db37fbff05a3267199e6295ed9f Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 9 Oct 2023 20:36:49 +0200 Subject: [PATCH 07/18] Do not restore session with invalid token. --- .../matrix/api/auth/MatrixAuthenticationService.kt | 5 +++++ .../matrix/impl/auth/RustMatrixAuthenticationService.kt | 6 +++++- 2 files changed, 10 insertions(+), 1 deletion(-) 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 981ee028b2..501b40508e 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 @@ -25,6 +25,11 @@ import kotlinx.coroutines.flow.StateFlow interface MatrixAuthenticationService { fun loggedInStateFlow(): Flow suspend fun getLatestSessionId(): SessionId? + + /** + * Restore a session from a [sessionId]. + * Do not restore anything it the access token is not valid anymore. + */ suspend fun restoreSession(sessionId: SessionId): Result fun getHomeserverDetails(): StateFlow suspend fun setHomeserver(homeserver: String): 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 a0d924e945..033a5f6073 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 @@ -76,7 +76,11 @@ class RustMatrixAuthenticationService @Inject constructor( runCatching { val sessionData = sessionStore.getSession(sessionId.value) if (sessionData != null) { - rustMatrixClientFactory.create(sessionData) + if (sessionData.isTokenValid) { + rustMatrixClientFactory.create(sessionData) + } else { + error("Token is not valid") + } } else { error("No session to restore with id $sessionId") } From 1a99fd9a627aa75f3362c022840060f3bd6d7743 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 9 Oct 2023 21:41:07 +0200 Subject: [PATCH 08/18] Fix compilation issue in sample app. --- .../kotlin/io/element/android/samples/minimal/MainActivity.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 035d5a3e3b..c95a44d8ea 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 @@ -65,8 +65,8 @@ class MainActivity : ComponentActivity() { WindowCompat.setDecorFitsSystemWindows(window, false) setContent { ElementTheme { - val isLoggedIn by matrixAuthenticationService.isLoggedIn().collectAsState(initial = LoggedInState.NotLoggedIn) - Content(isLoggedIn = isLoggedIn is LoggedInState.LoggedIn, modifier = Modifier.fillMaxSize()) + val loggedInState by matrixAuthenticationService.loggedInStateFlow().collectAsState(initial = LoggedInState.NotLoggedIn) + Content(isLoggedIn = loggedInState is LoggedInState.LoggedIn, modifier = Modifier.fillMaxSize()) } } From 7e77e6e063d36e1161ae2795f1cfb1c29e1e63c6 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 9 Oct 2023 21:42:04 +0200 Subject: [PATCH 09/18] Changelog --- changelog.d/1520.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/1520.feature diff --git a/changelog.d/1520.feature b/changelog.d/1520.feature new file mode 100644 index 0000000000..eaf99776d6 --- /dev/null +++ b/changelog.d/1520.feature @@ -0,0 +1 @@ +Improve deleted session behavior. From c3a0deb2014a4926e5bb62522b4d117559c0e220 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 9 Oct 2023 21:47:41 +0200 Subject: [PATCH 10/18] Code quality. --- .../src/main/kotlin/io/element/android/appnav/RootFlowNode.kt | 2 +- .../io/element/android/appnav/signedout/SignedOutView.kt | 4 ++-- .../element/android/libraries/sessionstorage/api/LoginType.kt | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) 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 6be25d5bc2..7936912762 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt @@ -107,7 +107,7 @@ class RootFlowNode @AssistedInject constructor( onFailure = { switchToNotLoggedInFlow() } ) } else { - switchToSignedOutFlow(SessionId((navState.loggedInState.sessionId))) + switchToSignedOutFlow(SessionId(navState.loggedInState.sessionId)) } } LoggedInState.NotLoggedIn -> { diff --git a/appnav/src/main/kotlin/io/element/android/appnav/signedout/SignedOutView.kt b/appnav/src/main/kotlin/io/element/android/appnav/signedout/SignedOutView.kt index 7c31e7713b..badb2f83ee 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/signedout/SignedOutView.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/signedout/SignedOutView.kt @@ -71,7 +71,7 @@ fun SignedOutView( } @Composable -fun SignedOutHeader() { +private fun SignedOutHeader() { IconTitleSubtitleMolecule( modifier = Modifier.padding(top = 60.dp, bottom = 12.dp), title = "You’re signed out", @@ -144,7 +144,7 @@ private fun SignedOutFooter( @PreviewsDayNight @Composable -fun SignedOutViewPreview( +internal fun SignedOutViewPreview( @PreviewParameter(SignedOutStateProvider::class) state: SignedOutState, ) = ElementPreview { SignedOutView( diff --git a/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/LoginType.kt b/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/LoginType.kt index a1e4400797..ce29e3729a 100644 --- a/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/LoginType.kt +++ b/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/LoginType.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 The Matrix.org Foundation C.I.C. + * 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. From 6761a5e2fbda5bf10c0bad6bb26cfb5b57550927 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 9 Oct 2023 22:13:48 +0200 Subject: [PATCH 11/18] Fix test compilation --- .../libraries/sessionstorage/impl/DatabaseSessionStoreTests.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 5126082b6f..fbb1daf097 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 @@ -68,7 +68,7 @@ class DatabaseSessionStoreTests { databaseSessionStore.isLoggedIn().test { assertThat(awaitItem()).isEqualTo(LoggedInState.NotLoggedIn) database.sessionDataQueries.insertSessionData(aSessionData) - assertThat(awaitItem()).isEqualTo(LoggedInState.LoggedIn(true)) + assertThat(awaitItem()).isEqualTo(LoggedInState.LoggedIn(sessionId = aSessionData.userId, isTokenValid = true)) database.sessionDataQueries.removeSession(aSessionData.userId) assertThat(awaitItem()).isEqualTo(LoggedInState.NotLoggedIn) } From 96867bccf20f3cb22e33b34d78117d6355f0a789 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 10 Oct 2023 09:55:47 +0200 Subject: [PATCH 12/18] Database version 4: add isTokenValid and loginType fields Generate database with model version 4 --- .../sessionstorage/impl/DatabaseSessionStore.kt | 2 +- .../sessionstorage/impl/SessionDataMapper.kt | 2 +- .../impl/src/main/sqldelight/databases/4.db | Bin 0 -> 12288 bytes .../libraries/matrix/session/SessionData.sq | 4 +++- .../impl/src/main/sqldelight/migrations/3.sqm | 2 ++ .../impl/DatabaseSessionStoreTests.kt | 6 +++--- 6 files changed, 10 insertions(+), 6 deletions(-) create mode 100644 libraries/session-storage/impl/src/main/sqldelight/databases/4.db create mode 100644 libraries/session-storage/impl/src/main/sqldelight/migrations/3.sqm 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 1592e7646e..0ca63d52a7 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 @@ -46,7 +46,7 @@ class DatabaseSessionStore @Inject constructor( } else { LoggedInState.LoggedIn( sessionId = it.userId, - isTokenValid = (it.isTokenValid ?: 1) == 1L + isTokenValid = it.isTokenValid == 1L ) } } diff --git a/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/SessionDataMapper.kt b/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/SessionDataMapper.kt index 7c00d98f91..1a81647f5c 100644 --- a/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/SessionDataMapper.kt +++ b/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/SessionDataMapper.kt @@ -46,7 +46,7 @@ internal fun DbSessionData.toApiModel(): SessionData { oidcData = oidcData, slidingSyncProxy = slidingSyncProxy, loginTimestamp = loginTimestamp?.let { Date(it) }, - isTokenValid = (isTokenValid ?: 1) == 1L, + isTokenValid = isTokenValid == 1L, loginType = LoginType.fromName(loginType ?: LoginType.UNKNOWN.name), ) } diff --git a/libraries/session-storage/impl/src/main/sqldelight/databases/4.db b/libraries/session-storage/impl/src/main/sqldelight/databases/4.db new file mode 100644 index 0000000000000000000000000000000000000000..f2e7eb964f4cd9384e8858a2b768500082887ce0 GIT binary patch literal 12288 zcmeI#K}*9h6bJBhips!kw_O9fD2O29-Kj;&>YQCC+iAo$-GW^++iq@G@dNmk{8k=K zTBcQmogM!`+P=P*wE4A{bnk4DavI2Z!cuxlYQ!+eIi-XUL-j4yS53#;t3aJ~ysjDK zxb?PQ{UD`EKq^7?ecb@$ApijgKmY;|fB*y_009U<;6Dqz^o`wmqhY*0r|co*U7jRD zMprClh0D^VV>Vrry3GsQq=f`MC{xvD36CwGx@O;{?OWA*woN;Zb<=bPv}F#CvMQg? zg~xx_dCHrz{}fB*y_009U<00Izz00bbg ksRCuaFzo*~_48s~5P$##AOHafKmY;|fB*y_0D(;46J-*Fv;Y7A literal 0 HcmV?d00001 diff --git a/libraries/session-storage/impl/src/main/sqldelight/io/element/android/libraries/matrix/session/SessionData.sq b/libraries/session-storage/impl/src/main/sqldelight/io/element/android/libraries/matrix/session/SessionData.sq index 05049c5635..8cebfbe5e2 100644 --- a/libraries/session-storage/impl/src/main/sqldelight/io/element/android/libraries/matrix/session/SessionData.sq +++ b/libraries/session-storage/impl/src/main/sqldelight/io/element/android/libraries/matrix/session/SessionData.sq @@ -6,7 +6,9 @@ CREATE TABLE SessionData ( homeserverUrl TEXT NOT NULL, slidingSyncProxy TEXT, loginTimestamp INTEGER, - oidcData TEXT + oidcData TEXT, + isTokenValid INTEGER NOT NULL, + loginType TEXT ); diff --git a/libraries/session-storage/impl/src/main/sqldelight/migrations/3.sqm b/libraries/session-storage/impl/src/main/sqldelight/migrations/3.sqm new file mode 100644 index 0000000000..c4d0743ff5 --- /dev/null +++ b/libraries/session-storage/impl/src/main/sqldelight/migrations/3.sqm @@ -0,0 +1,2 @@ +ALTER TABLE SessionData ADD COLUMN isTokenValid INTEGER NOT NULL DEFAULT 1; +ALTER TABLE SessionData ADD COLUMN loginType TEXT; 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 fbb1daf097..ab0b41194e 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 @@ -39,7 +39,7 @@ class DatabaseSessionStoreTests { slidingSyncProxy = null, loginTimestamp = null, oidcData = "aOidcData", - isTokenValid = null, + isTokenValid = 1, loginType = null, ) @@ -124,7 +124,7 @@ class DatabaseSessionStoreTests { slidingSyncProxy = "slidingSyncProxy", loginTimestamp = 1, oidcData = "aOidcData", - isTokenValid = null, + isTokenValid = 1, loginType = null, ) val secondSessionData = SessionData( @@ -136,7 +136,7 @@ class DatabaseSessionStoreTests { slidingSyncProxy = "slidingSyncProxyAltered", loginTimestamp = 2, oidcData = "aOidcDataAltered", - isTokenValid = null, + isTokenValid = 1, loginType = null, ) assertThat(firstSessionData.userId).isEqualTo(secondSessionData.userId) From 8bbcb973c46a79d577dc38e8efd1189dba8e14be Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 10 Oct 2023 10:20:23 +0200 Subject: [PATCH 13/18] Fix test. --- .../libraries/sessionstorage/impl/DatabaseSessionStoreTests.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 ab0b41194e..4ca932f48f 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 @@ -21,6 +21,7 @@ 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 io.element.android.libraries.sessionstorage.api.LoginType import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test @@ -40,7 +41,7 @@ class DatabaseSessionStoreTests { loginTimestamp = null, oidcData = "aOidcData", isTokenValid = 1, - loginType = null, + loginType = LoginType.UNKNOWN.name, ) @Before From 8f1ccfccf20dec1d58cc5d4b4a3db79ec05978d1 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 10 Oct 2023 20:04:34 +0200 Subject: [PATCH 14/18] Move SignedOut classes to their own module. --- appnav/build.gradle.kts | 1 - .../io/element/android/appnav/RootFlowNode.kt | 12 +++-- features/signedout/api/build.gradle.kts | 28 ++++++++++ .../signedout/api/SignedOutEntryPoint.kt | 37 +++++++++++++ features/signedout/impl/build.gradle.kts | 52 +++++++++++++++++++ .../impl/DefaultSignedOutEntryPoint.kt | 46 ++++++++++++++++ .../signedout/impl}/SignedOutEvents.kt | 2 +- .../features/signedout/impl}/SignedOutNode.kt | 2 +- .../signedout/impl}/SignedOutPresenter.kt | 2 +- .../signedout/impl}/SignedOutState.kt | 2 +- .../signedout/impl}/SignedOutStateProvider.kt | 2 +- .../features/signedout/impl}/SignedOutView.kt | 3 +- .../signedout/impl}/SignedOutPresenterTest.kt | 2 +- 13 files changed, 179 insertions(+), 12 deletions(-) create mode 100644 features/signedout/api/build.gradle.kts create mode 100644 features/signedout/api/src/main/kotlin/io/element/android/features/signedout/api/SignedOutEntryPoint.kt create mode 100644 features/signedout/impl/build.gradle.kts create mode 100644 features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/DefaultSignedOutEntryPoint.kt rename {appnav/src/main/kotlin/io/element/android/appnav/signedout => features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl}/SignedOutEvents.kt (93%) rename {appnav/src/main/kotlin/io/element/android/appnav/signedout => features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl}/SignedOutNode.kt (97%) rename {appnav/src/main/kotlin/io/element/android/appnav/signedout => features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl}/SignedOutPresenter.kt (97%) rename {appnav/src/main/kotlin/io/element/android/appnav/signedout => features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl}/SignedOutState.kt (94%) rename {appnav/src/main/kotlin/io/element/android/appnav/signedout => features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl}/SignedOutStateProvider.kt (97%) rename {appnav/src/main/kotlin/io/element/android/appnav/signedout => features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl}/SignedOutView.kt (98%) rename {appnav/src/test/kotlin/io/element/android/appnav/signedout => features/signedout/impl/src/test/kotlin/io/element/android/features/signedout/impl}/SignedOutPresenterTest.kt (98%) diff --git a/appnav/build.gradle.kts b/appnav/build.gradle.kts index f7d8540c4e..88f0741ebe 100644 --- a/appnav/build.gradle.kts +++ b/appnav/build.gradle.kts @@ -64,7 +64,6 @@ dependencies { testImplementation(libs.test.turbine) testImplementation(projects.libraries.matrix.test) testImplementation(projects.features.networkmonitor.test) - testImplementation(projects.libraries.sessionStorage.implMemory) testImplementation(projects.tests.testutils) testImplementation(projects.features.rageshake.test) testImplementation(projects.features.rageshake.impl) 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 7936912762..df98998ba5 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt @@ -42,10 +42,10 @@ import io.element.android.appnav.intent.ResolvedIntent import io.element.android.appnav.root.RootNavStateFlowFactory import io.element.android.appnav.root.RootPresenter import io.element.android.appnav.root.RootView -import io.element.android.appnav.signedout.SignedOutNode import io.element.android.features.login.api.oidc.OidcAction import io.element.android.features.login.api.oidc.OidcActionFlow import io.element.android.features.rageshake.api.bugreport.BugReportEntryPoint +import io.element.android.features.signedout.api.SignedOutEntryPoint import io.element.android.libraries.architecture.BackstackNode import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler import io.element.android.libraries.architecture.createNode @@ -71,6 +71,7 @@ class RootFlowNode @AssistedInject constructor( private val matrixClientsHolder: MatrixClientsHolder, private val presenter: RootPresenter, private val bugReportEntryPoint: BugReportEntryPoint, + private val signedOutEntryPoint: SignedOutEntryPoint, private val intentResolver: IntentResolver, private val oidcActionFlow: OidcActionFlow, ) : BackstackNode( @@ -217,8 +218,13 @@ class RootFlowNode @AssistedInject constructor( } NavTarget.NotLoggedInFlow -> createNode(buildContext) is NavTarget.SignedOutFlow -> { - val inputs = SignedOutNode.Inputs(navTarget.sessionId) - createNode(buildContext, listOf(inputs)) + signedOutEntryPoint.nodeBuilder(this, buildContext) + .params( + SignedOutEntryPoint.Params( + sessionId = navTarget.sessionId + ) + ) + .build() } NavTarget.SplashScreen -> splashNode(buildContext) NavTarget.BugReport -> { diff --git a/features/signedout/api/build.gradle.kts b/features/signedout/api/build.gradle.kts new file mode 100644 index 0000000000..d2815315f3 --- /dev/null +++ b/features/signedout/api/build.gradle.kts @@ -0,0 +1,28 @@ +/* + * 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. + */ + +plugins { + id("io.element.android-library") +} + +android { + namespace = "io.element.android.features.signedout.api" +} + +dependencies { + implementation(projects.libraries.architecture) + implementation(projects.libraries.matrix.api) +} diff --git a/features/signedout/api/src/main/kotlin/io/element/android/features/signedout/api/SignedOutEntryPoint.kt b/features/signedout/api/src/main/kotlin/io/element/android/features/signedout/api/SignedOutEntryPoint.kt new file mode 100644 index 0000000000..7a156998d1 --- /dev/null +++ b/features/signedout/api/src/main/kotlin/io/element/android/features/signedout/api/SignedOutEntryPoint.kt @@ -0,0 +1,37 @@ +/* + * 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.features.signedout.api + +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import io.element.android.libraries.architecture.FeatureEntryPoint +import io.element.android.libraries.matrix.api.core.SessionId + +interface SignedOutEntryPoint : FeatureEntryPoint { + + data class Params( + val sessionId: SessionId, + ) + + fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder + + interface NodeBuilder { + fun params(params: Params): NodeBuilder + fun build(): Node + } +} + diff --git a/features/signedout/impl/build.gradle.kts b/features/signedout/impl/build.gradle.kts new file mode 100644 index 0000000000..399c95e254 --- /dev/null +++ b/features/signedout/impl/build.gradle.kts @@ -0,0 +1,52 @@ +/* + * 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. + */ + +plugins { + id("io.element.android-compose-library") + alias(libs.plugins.anvil) + alias(libs.plugins.ksp) + id("kotlin-parcelize") +} + +android { + namespace = "io.element.android.features.signedout.impl" +} + +anvil { + generateDaggerFactories.set(true) +} + +dependencies { + implementation(projects.anvilannotations) + anvil(projects.anvilcodegen) + api(projects.features.signedout.api) + implementation(projects.libraries.core) + implementation(projects.libraries.architecture) + implementation(projects.libraries.matrix.api) + implementation(projects.libraries.matrixui) + implementation(projects.libraries.designsystem) + + testImplementation(libs.test.junit) + testImplementation(libs.coroutines.test) + testImplementation(libs.molecule.runtime) + testImplementation(libs.test.truth) + testImplementation(libs.test.turbine) + testImplementation(projects.libraries.matrix.test) + testImplementation(projects.libraries.sessionStorage.implMemory) + testImplementation(projects.tests.testutils) + + ksp(libs.showkase.processor) +} diff --git a/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/DefaultSignedOutEntryPoint.kt b/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/DefaultSignedOutEntryPoint.kt new file mode 100644 index 0000000000..59b19f041e --- /dev/null +++ b/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/DefaultSignedOutEntryPoint.kt @@ -0,0 +1,46 @@ +/* + * 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.features.signedout.impl + +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.features.signedout.api.SignedOutEntryPoint +import io.element.android.libraries.architecture.createNode +import io.element.android.libraries.di.AppScope +import javax.inject.Inject + +@ContributesBinding(AppScope::class) +class DefaultSignedOutEntryPoint @Inject constructor() : SignedOutEntryPoint { + + override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): SignedOutEntryPoint.NodeBuilder { + val plugins = ArrayList() + + return object : SignedOutEntryPoint.NodeBuilder { + + override fun params(params: SignedOutEntryPoint.Params): SignedOutEntryPoint.NodeBuilder { + plugins += SignedOutNode.Inputs(params.sessionId) + return this + } + + override fun build(): Node { + return parentNode.createNode(buildContext, plugins) + } + } + } +} diff --git a/appnav/src/main/kotlin/io/element/android/appnav/signedout/SignedOutEvents.kt b/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutEvents.kt similarity index 93% rename from appnav/src/main/kotlin/io/element/android/appnav/signedout/SignedOutEvents.kt rename to features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutEvents.kt index 3c3c61696b..a2057226a4 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/signedout/SignedOutEvents.kt +++ b/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutEvents.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.appnav.signedout +package io.element.android.features.signedout.impl sealed interface SignedOutEvents { data object SignInAgain : SignedOutEvents diff --git a/appnav/src/main/kotlin/io/element/android/appnav/signedout/SignedOutNode.kt b/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutNode.kt similarity index 97% rename from appnav/src/main/kotlin/io/element/android/appnav/signedout/SignedOutNode.kt rename to features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutNode.kt index 25917daab5..381daa7278 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/signedout/SignedOutNode.kt +++ b/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutNode.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.appnav.signedout +package io.element.android.features.signedout.impl import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier diff --git a/appnav/src/main/kotlin/io/element/android/appnav/signedout/SignedOutPresenter.kt b/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutPresenter.kt similarity index 97% rename from appnav/src/main/kotlin/io/element/android/appnav/signedout/SignedOutPresenter.kt rename to features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutPresenter.kt index 1292ad0496..ee8cf2e63f 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/signedout/SignedOutPresenter.kt +++ b/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutPresenter.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.appnav.signedout +package io.element.android.features.signedout.impl import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState diff --git a/appnav/src/main/kotlin/io/element/android/appnav/signedout/SignedOutState.kt b/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutState.kt similarity index 94% rename from appnav/src/main/kotlin/io/element/android/appnav/signedout/SignedOutState.kt rename to features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutState.kt index f181c537ba..2f7af29e40 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/signedout/SignedOutState.kt +++ b/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutState.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.appnav.signedout +package io.element.android.features.signedout.impl import io.element.android.libraries.sessionstorage.api.SessionData diff --git a/appnav/src/main/kotlin/io/element/android/appnav/signedout/SignedOutStateProvider.kt b/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutStateProvider.kt similarity index 97% rename from appnav/src/main/kotlin/io/element/android/appnav/signedout/SignedOutStateProvider.kt rename to features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutStateProvider.kt index b7a81cf86b..1af0ef41de 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/signedout/SignedOutStateProvider.kt +++ b/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutStateProvider.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.appnav.signedout +package io.element.android.features.signedout.impl import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.libraries.matrix.api.core.SessionId diff --git a/appnav/src/main/kotlin/io/element/android/appnav/signedout/SignedOutView.kt b/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutView.kt similarity index 98% rename from appnav/src/main/kotlin/io/element/android/appnav/signedout/SignedOutView.kt rename to features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutView.kt index badb2f83ee..42c38224b6 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/signedout/SignedOutView.kt +++ b/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutView.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.appnav.signedout +package io.element.android.features.signedout.impl import androidx.activity.compose.BackHandler import androidx.compose.foundation.background @@ -48,7 +48,6 @@ import io.element.android.libraries.designsystem.utils.CommonDrawables import io.element.android.libraries.theme.ElementTheme import kotlinx.collections.immutable.persistentListOf -// TODO i18n, when wording has been approved. @Composable fun SignedOutView( state: SignedOutState, diff --git a/appnav/src/test/kotlin/io/element/android/appnav/signedout/SignedOutPresenterTest.kt b/features/signedout/impl/src/test/kotlin/io/element/android/features/signedout/impl/SignedOutPresenterTest.kt similarity index 98% rename from appnav/src/test/kotlin/io/element/android/appnav/signedout/SignedOutPresenterTest.kt rename to features/signedout/impl/src/test/kotlin/io/element/android/features/signedout/impl/SignedOutPresenterTest.kt index 529123250c..aa064a7214 100644 --- a/appnav/src/test/kotlin/io/element/android/appnav/signedout/SignedOutPresenterTest.kt +++ b/features/signedout/impl/src/test/kotlin/io/element/android/features/signedout/impl/SignedOutPresenterTest.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.appnav.signedout +package io.element.android.features.signedout.impl import app.cash.molecule.RecompositionMode import app.cash.molecule.moleculeFlow From 257927f4f581b4e46af01f9de57afedd109655d0 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 10 Oct 2023 20:09:05 +0200 Subject: [PATCH 15/18] Import strings for SigneddOut module. --- features/signedout/impl/src/main/res/values/localazy.xml | 8 ++++++++ libraries/ui-strings/src/main/res/values/localazy.xml | 1 + tools/localazy/config.json | 6 ++++++ 3 files changed, 15 insertions(+) create mode 100644 features/signedout/impl/src/main/res/values/localazy.xml diff --git a/features/signedout/impl/src/main/res/values/localazy.xml b/features/signedout/impl/src/main/res/values/localazy.xml new file mode 100644 index 0000000000..bfbad719c7 --- /dev/null +++ b/features/signedout/impl/src/main/res/values/localazy.xml @@ -0,0 +1,8 @@ + + + "You’ve changed your password on another session" + "You have deleted the session from another session" + "Your server’s administrator has invalidated your access" + "You might have been signed out for one of the reasons listed below. Please sign in again to continue using Element." + "You’re signed out" + diff --git a/libraries/ui-strings/src/main/res/values/localazy.xml b/libraries/ui-strings/src/main/res/values/localazy.xml index da48ac9453..6a7f0ecbb7 100644 --- a/libraries/ui-strings/src/main/res/values/localazy.xml +++ b/libraries/ui-strings/src/main/res/values/localazy.xml @@ -64,6 +64,7 @@ "Send message" "Share" "Share link" + "Sign in again" "Skip" "Start" "Start chat" diff --git a/tools/localazy/config.json b/tools/localazy/config.json index 3842198da6..7e07d269c0 100644 --- a/tools/localazy/config.json +++ b/tools/localazy/config.json @@ -25,6 +25,12 @@ "screen_onboarding_.*" ] }, + { + "name": ":features:signedout:impl", + "includeRegex": [ + "screen_signed_out_.*" + ] + }, { "name": ":features:invitelist:impl", "includeRegex": [ From 8be6bad4658dfbdc6d0c516a4b062966dc73efd1 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 10 Oct 2023 20:59:58 +0200 Subject: [PATCH 16/18] SignedOutView: iterate on design. --- features/signedout/impl/build.gradle.kts | 1 + .../signedout/impl/SignedOutPresenter.kt | 3 ++ .../features/signedout/impl/SignedOutState.kt | 1 + .../signedout/impl/SignedOutStateProvider.kt | 3 +- .../features/signedout/impl/SignedOutView.kt | 45 ++++++++++--------- .../impl/src/main/res/drawable/ic_devices.xml | 29 ++++++++++++ .../main/res/drawable/ic_do_disturb_alt.xml | 29 ++++++++++++ .../src/main/res/drawable/ic_lock_outline.xml | 29 ++++++++++++ .../impl/src/main/res/values/localazy.xml | 2 +- 9 files changed, 118 insertions(+), 24 deletions(-) create mode 100644 features/signedout/impl/src/main/res/drawable/ic_devices.xml create mode 100644 features/signedout/impl/src/main/res/drawable/ic_do_disturb_alt.xml create mode 100644 features/signedout/impl/src/main/res/drawable/ic_lock_outline.xml diff --git a/features/signedout/impl/build.gradle.kts b/features/signedout/impl/build.gradle.kts index 399c95e254..0255ac3ca1 100644 --- a/features/signedout/impl/build.gradle.kts +++ b/features/signedout/impl/build.gradle.kts @@ -38,6 +38,7 @@ dependencies { implementation(projects.libraries.matrix.api) implementation(projects.libraries.matrixui) implementation(projects.libraries.designsystem) + implementation(projects.libraries.uiStrings) testImplementation(libs.test.junit) testImplementation(libs.coroutines.test) diff --git a/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutPresenter.kt b/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutPresenter.kt index ee8cf2e63f..a93d22253d 100644 --- a/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutPresenter.kt +++ b/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutPresenter.kt @@ -26,12 +26,14 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.sessionstorage.api.SessionStore import kotlinx.coroutines.launch class SignedOutPresenter @AssistedInject constructor( @Assisted private val sessionId: String, /* Cannot inject SessionId */ private val sessionStore: SessionStore, + private val buildMeta: BuildMeta, ) : Presenter { @AssistedFactory @@ -56,6 +58,7 @@ class SignedOutPresenter @AssistedInject constructor( } return SignedOutState( + appName = buildMeta.applicationName, signedOutSession = signedOutSession, eventSink = ::handleEvents ) diff --git a/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutState.kt b/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutState.kt index 2f7af29e40..eff0ab6d5b 100644 --- a/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutState.kt +++ b/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutState.kt @@ -20,6 +20,7 @@ import io.element.android.libraries.sessionstorage.api.SessionData // Do not use default value, so no member get forgotten in the presenters. data class SignedOutState( + val appName: String, val signedOutSession: SessionData?, val eventSink: (SignedOutEvents) -> Unit, ) diff --git a/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutStateProvider.kt b/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutStateProvider.kt index 1af0ef41de..0182e87cf3 100644 --- a/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutStateProvider.kt +++ b/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutStateProvider.kt @@ -30,8 +30,9 @@ open class SignedOutStateProvider : PreviewParameterProvider { } fun aSignedOutState() = SignedOutState( + appName = "AppName", + signedOutSession = aSessionData(), eventSink = {}, - signedOutSession = aSessionData() ) fun aSessionData( diff --git a/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutView.kt b/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutView.kt index 42c38224b6..63845ead90 100644 --- a/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutView.kt +++ b/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutView.kt @@ -17,7 +17,7 @@ package io.element.android.features.signedout.impl import androidx.activity.compose.BackHandler -import androidx.compose.foundation.background +import androidx.annotation.DrawableRes import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth @@ -25,13 +25,12 @@ import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.systemBarsPadding -import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.AccountCircle -import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.BiasAlignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule @@ -44,8 +43,8 @@ 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.Icon import io.element.android.libraries.designsystem.theme.temporaryColorBgSpecial -import io.element.android.libraries.designsystem.utils.CommonDrawables import io.element.android.libraries.theme.ElementTheme +import io.element.android.libraries.ui.strings.CommonStrings import kotlinx.collections.immutable.persistentListOf @Composable @@ -59,7 +58,7 @@ fun SignedOutView( .fillMaxSize() .systemBarsPadding() .imePadding(), - header = { SignedOutHeader() }, + header = { SignedOutHeader(state) }, content = { SignedOutContent() }, footer = { SignedOutFooter( @@ -70,12 +69,13 @@ fun SignedOutView( } @Composable -private fun SignedOutHeader() { +private fun SignedOutHeader(state: SignedOutState) { IconTitleSubtitleMolecule( modifier = Modifier.padding(top = 60.dp, bottom = 12.dp), - title = "You’re signed out", - subTitle = "It can be due to various reasons:", - iconImageVector = Icons.Filled.AccountCircle + title = stringResource(id = R.string.screen_signed_out_title), + subTitle = stringResource(id = R.string.screen_signed_out_subtitle, state.appName), + iconImageVector = Icons.Filled.AccountCircle, + iconTint = ElementTheme.colors.iconSecondary, ) } @@ -93,16 +93,16 @@ private fun SignedOutContent( InfoListOrganism( items = persistentListOf( InfoListItem( - message = "You’ve changed your password on another session.", - iconComposable = { CheckIcon() }, + message = stringResource(id = R.string.screen_signed_out_reason_1), + iconComposable = { Icon(R.drawable.ic_lock_outline) }, ), InfoListItem( - message = "You have deleted this session from another session.", - iconComposable = { CheckIcon() }, + message = stringResource(id = R.string.screen_signed_out_reason_2), + iconComposable = { Icon(R.drawable.ic_devices) }, ), InfoListItem( - message = "The administrator of your server has invalidated your access for security reason.", - iconComposable = { CheckIcon() }, + message = stringResource(id = R.string.screen_signed_out_reason_3), + iconComposable = { Icon(R.drawable.ic_do_disturb_alt) }, ), ), textStyle = ElementTheme.typography.fontBodyMdMedium, @@ -113,15 +113,16 @@ private fun SignedOutContent( } @Composable -private fun CheckIcon(modifier: Modifier = Modifier) { +private fun Icon( + @DrawableRes iconResourceId: Int, + modifier: Modifier = Modifier, +) { Icon( modifier = modifier - .size(20.dp) - .background(color = MaterialTheme.colorScheme.background, shape = CircleShape) - .padding(2.dp), - resourceId = CommonDrawables.ic_compound_check, + .size(20.dp), + resourceId = iconResourceId, contentDescription = null, - tint = ElementTheme.colors.textActionAccent, + tint = ElementTheme.colors.iconSecondary, ) } @@ -134,7 +135,7 @@ private fun SignedOutFooter( modifier = modifier, ) { Button( - text = "Sign in again", + text = stringResource(id = CommonStrings.action_sign_in_again), onClick = onSignInAgain, modifier = Modifier.fillMaxWidth(), ) diff --git a/features/signedout/impl/src/main/res/drawable/ic_devices.xml b/features/signedout/impl/src/main/res/drawable/ic_devices.xml new file mode 100644 index 0000000000..e01de99a1d --- /dev/null +++ b/features/signedout/impl/src/main/res/drawable/ic_devices.xml @@ -0,0 +1,29 @@ + + + + + + + + diff --git a/features/signedout/impl/src/main/res/drawable/ic_do_disturb_alt.xml b/features/signedout/impl/src/main/res/drawable/ic_do_disturb_alt.xml new file mode 100644 index 0000000000..2ac90bd377 --- /dev/null +++ b/features/signedout/impl/src/main/res/drawable/ic_do_disturb_alt.xml @@ -0,0 +1,29 @@ + + + + + + + + diff --git a/features/signedout/impl/src/main/res/drawable/ic_lock_outline.xml b/features/signedout/impl/src/main/res/drawable/ic_lock_outline.xml new file mode 100644 index 0000000000..51831bf3c1 --- /dev/null +++ b/features/signedout/impl/src/main/res/drawable/ic_lock_outline.xml @@ -0,0 +1,29 @@ + + + + + + + + diff --git a/features/signedout/impl/src/main/res/values/localazy.xml b/features/signedout/impl/src/main/res/values/localazy.xml index bfbad719c7..f70b0042d5 100644 --- a/features/signedout/impl/src/main/res/values/localazy.xml +++ b/features/signedout/impl/src/main/res/values/localazy.xml @@ -3,6 +3,6 @@ "You’ve changed your password on another session" "You have deleted the session from another session" "Your server’s administrator has invalidated your access" - "You might have been signed out for one of the reasons listed below. Please sign in again to continue using Element." + "You might have been signed out for one of the reasons listed below. Please sign in again to continue using %s." "You’re signed out" From 381a4996c6269f10490bf6b77a2721367002a38d Mon Sep 17 00:00:00 2001 From: ElementBot Date: Tue, 10 Oct 2023 19:18:36 +0000 Subject: [PATCH 17/18] Update screenshots --- ...ut.impl_null_SignedOutView-D-0_0_null_0,NEXUS_5,1.0,en].png | 3 +++ ...ut.impl_null_SignedOutView-N-0_1_null_0,NEXUS_5,1.0,en].png | 3 +++ 2 files changed, 6 insertions(+) create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.signedout.impl_null_SignedOutView-D-0_0_null_0,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.signedout.impl_null_SignedOutView-N-0_1_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.signedout.impl_null_SignedOutView-D-0_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.signedout.impl_null_SignedOutView-D-0_0_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..82a7ca445f --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.signedout.impl_null_SignedOutView-D-0_0_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:25d69103d52132a0d47f3e5c4feebc4a981e4d382255e312c83973fff4e0b3e8 +size 60658 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.signedout.impl_null_SignedOutView-N-0_1_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.signedout.impl_null_SignedOutView-N-0_1_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..32059527ae --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.signedout.impl_null_SignedOutView-N-0_1_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5844f66e9c07ba824fb3d1871281fc385ec7c0898cae0bf8e532bea1ef2278fb +size 58885 From 33943b2ae82770e32d5761e58b5155090de00e44 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 10 Oct 2023 22:21:02 +0200 Subject: [PATCH 18/18] Fix compilation issue and improve test. --- .../features/signedout/impl/SignedOutPresenterTest.kt | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/features/signedout/impl/src/test/kotlin/io/element/android/features/signedout/impl/SignedOutPresenterTest.kt b/features/signedout/impl/src/test/kotlin/io/element/android/features/signedout/impl/SignedOutPresenterTest.kt index aa064a7214..208d21154e 100644 --- a/features/signedout/impl/src/test/kotlin/io/element/android/features/signedout/impl/SignedOutPresenterTest.kt +++ b/features/signedout/impl/src/test/kotlin/io/element/android/features/signedout/impl/SignedOutPresenterTest.kt @@ -22,6 +22,7 @@ import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.test.A_SESSION_ID +import io.element.android.libraries.matrix.test.core.aBuildMeta import io.element.android.libraries.sessionstorage.api.SessionStore import io.element.android.libraries.sessionstorage.impl.memory.InMemorySessionStore import io.element.android.tests.testutils.WarmUpRule @@ -33,18 +34,21 @@ class SignedOutPresenterTest { @get:Rule val warmUpRule = WarmUpRule() + private val appName = "AppName" + @Test fun `present - initial state`() = runTest { val aSessionData = aSessionData() val sessionStore = InMemorySessionStore().apply { storeData(aSessionData) } - val presenter = createPresenter(sessionStore = sessionStore) + val presenter = createSignedOutPresenter(sessionStore = sessionStore) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { skipItems(1) val initialState = awaitItem() + assertThat(initialState.appName).isEqualTo(appName) assertThat(initialState.signedOutSession).isEqualTo(aSessionData) } } @@ -55,7 +59,7 @@ class SignedOutPresenterTest { val sessionStore = InMemorySessionStore().apply { storeData(aSessionData) } - val presenter = createPresenter(sessionStore = sessionStore) + val presenter = createSignedOutPresenter(sessionStore = sessionStore) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -69,13 +73,14 @@ class SignedOutPresenterTest { } } - private fun createPresenter( + private fun createSignedOutPresenter( sessionId: SessionId = A_SESSION_ID, sessionStore: SessionStore = InMemorySessionStore(), ): SignedOutPresenter { return SignedOutPresenter( sessionId = sessionId.value, sessionStore = sessionStore, + buildMeta = aBuildMeta(applicationName = appName), ) } }