diff --git a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt index 2f0988281e..731c26f684 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt @@ -93,7 +93,6 @@ import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias import io.element.android.libraries.matrix.api.permalink.PermalinkData import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.api.sync.SyncService -import io.element.android.libraries.matrix.api.verification.SessionVerificationServiceListener import io.element.android.libraries.matrix.api.verification.VerificationRequest import io.element.android.libraries.preferences.api.store.AppPreferencesStore import io.element.android.libraries.push.api.notifications.conversations.NotificationConversationService @@ -111,14 +110,10 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withTimeout import kotlinx.parcelize.Parcelize import timber.log.Timber -import java.time.Duration -import java.time.Instant import java.util.Optional import java.util.UUID import kotlin.time.Duration.Companion.milliseconds -import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.seconds -import kotlin.time.toKotlinDuration import im.vector.app.features.analytics.plan.JoinedRoom as JoinedRoomAnalyticsEvent // The maximum number of room nodes that should be kept in the backstack at the same time. @@ -185,36 +180,20 @@ class LoggedInFlowNode( roomMembershipObserver = matrixClient.roomMembershipObserver, ) - private val verificationListener = object : SessionVerificationServiceListener { - override fun onIncomingSessionRequest(verificationRequest: VerificationRequest.Incoming) { - // Without this launch the rendering and actual state of this Appyx node's children gets out of sync, resulting in a crash. - // This might be because this method is called back from Rust in a background thread. - lifecycleScope.launch { - val receivedAt = Instant.now() - - // Wait until the app is in foreground to display the incoming verification request - appNavigationStateService.appNavigationState.first { it.isInForeground } - - // TODO there should also be a timeout for > 10 minutes elapsed since the request was created, but the SDK doesn't expose that info yet - val now = Instant.now() - val elapsedTimeSinceReceived = Duration.between(receivedAt, now).toKotlinDuration() - - // Discard the incoming verification request if it has timed out - if (elapsedTimeSinceReceived > 2.minutes) { - Timber.w("Incoming verification request ${verificationRequest.details.flowId} discarded due to timeout.") - return@launch + /** + * Display the incoming verification request, the app is expected to be in foreground. + */ + fun onIncomingVerificationRequest(verificationRequest: VerificationRequest.Incoming) { + lifecycleScope.launch { + // Wait for the RoomList UI to be ready so the incoming verification screen can be displayed on top of it + // Otherwise, the RoomList UI may be incorrectly displayed on top + withTimeout(5.seconds) { + backstack.elements.first { elements -> + elements.any { it.key.navTarget == NavTarget.Home } } - - // Wait for the RoomList UI to be ready so the incoming verification screen can be displayed on top of it - // Otherwise, the RoomList UI may be incorrectly displayed on top - withTimeout(5.seconds) { - backstack.elements.first { elements -> - elements.any { it.key.navTarget == NavTarget.Home } - } - } - - backstack.singleTop(NavTarget.IncomingVerificationRequest(verificationRequest)) } + + backstack.singleTop(NavTarget.IncomingVerificationRequest(verificationRequest)) } } @@ -229,7 +208,6 @@ class LoggedInFlowNode( analyticsRoomListStateWatcher.start() appNavigationStateService.onNavigateToSession(id, matrixClient.sessionId) loggedInFlowProcessor.observeEvents(sessionCoroutineScope) - matrixClient.sessionVerificationService.setListener(verificationListener) mediaPreviewConfigMigration() sessionCoroutineScope.launch { // Wait for the network to be connected before pre-fetching the max file upload size @@ -258,7 +236,6 @@ class LoggedInFlowNode( onDestroy = { appNavigationStateService.onLeavingSession(id) loggedInFlowProcessor.stopObserving() - matrixClient.sessionVerificationService.setListener(null) analyticsRoomListStateWatcher.stop() } ) 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 5f2dc837ad..d8103c18c0 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt @@ -40,6 +40,9 @@ 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.session.MatrixSessionCache +import io.element.android.appnav.verification.IncomingVerificationRequestData +import io.element.android.appnav.verification.IncomingVerificationRequestObserver +import io.element.android.appnav.verification.OtherSessionIncomingVerificationNode import io.element.android.features.announcement.api.AnnouncementService import io.element.android.features.login.api.LoginParams import io.element.android.features.login.api.accesscontrol.AccountProviderAccessControl @@ -52,6 +55,7 @@ import io.element.android.libraries.architecture.BaseFlowNode import io.element.android.libraries.architecture.appyx.rememberDelegateTransitionHandler import io.element.android.libraries.architecture.createNode import io.element.android.libraries.architecture.waitForChildAttached +import io.element.android.libraries.core.coroutine.withPreviousValue import io.element.android.libraries.deeplink.api.DeeplinkData import io.element.android.libraries.di.annotations.AppCoroutineScope import io.element.android.libraries.featureflag.api.FeatureFlagService @@ -63,6 +67,7 @@ import io.element.android.libraries.matrix.api.core.ThreadId import io.element.android.libraries.matrix.api.core.asEventId import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias import io.element.android.libraries.matrix.api.permalink.PermalinkData +import io.element.android.libraries.matrix.api.verification.VerificationRequest import io.element.android.libraries.oauth.api.OAuthAction import io.element.android.libraries.oauth.api.OAuthActionFlow import io.element.android.libraries.sessionstorage.api.LoggedInState @@ -75,11 +80,16 @@ import io.element.android.services.appnavstate.api.ROOM_OPENED_FROM_NOTIFICATION import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.drop +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch +import kotlinx.coroutines.withTimeoutOrNull import kotlinx.parcelize.Parcelize import timber.log.Timber +import kotlin.time.Duration.Companion.seconds @ContributesNode(AppScope::class) @AssistedInject @@ -100,6 +110,7 @@ class RootFlowNode( private val announcementService: AnnouncementService, private val analyticsService: AnalyticsService, private val analyticsColdStartWatcher: AnalyticsColdStartWatcher, + private val incomingVerificationRequestObserver: IncomingVerificationRequestObserver, @AppCoroutineScope private val appCoroutineScope: CoroutineScope, ) : BaseFlowNode( backstack = BackStack( @@ -127,6 +138,8 @@ class RootFlowNode( observeNavState(false) } } + observeIncomingVerificationRequests() + observeRemovedSessions() super.onBuilt() } @@ -203,6 +216,65 @@ class RootFlowNode( private fun switchToLoggedInFlow(sessionId: SessionId, navId: Int) { pendingLoginParams = null backstack.safeRoot(NavTarget.LoggedInFlow(sessionId, navId)) + restoreAllSessions() + } + + /** + * Restore all the sessions with a valid token, so that they sync and can receive incoming verification requests. + */ + private fun restoreAllSessions() = lifecycleScope.launch { + sessionStore.getAllSessions() + .filter { it.isTokenValid } + .forEach { sessionData -> + matrixSessionCache.getOrRestore(SessionId(sessionData.userId)).onFailure { + Timber.e(it, "Failed to restore session ${sessionData.userId}") + } + } + } + + /** + * Remove from the cache the sessions which have been removed from the session store, for instance after a logout. + */ + private fun observeRemovedSessions() { + sessionStore.sessionsFlow() + .map { sessions -> sessions.map { SessionId(it.userId) }.toSet() } + .withPreviousValue() + .onEach { (previous, current) -> + (previous.orEmpty() - current).forEach { sessionId -> + Timber.d("Session $sessionId removed, remove it from the cache") + matrixSessionCache.remove(sessionId) + } + } + .launchIn(lifecycleScope) + } + + private fun observeIncomingVerificationRequests() { + incomingVerificationRequestObserver.incomingVerificationRequests() + .onEach(::onIncomingVerificationRequest) + .launchIn(lifecycleScope) + } + + private fun onIncomingVerificationRequest(data: IncomingVerificationRequestData) = lifecycleScope.launch { + // Wait for a logged in flow to be displayed + val currentSessionId = withTimeoutOrNull(5.seconds) { + backstack.elements.map { elements -> + elements.firstNotNullOfOrNull { (it.key.navTarget as? NavTarget.LoggedInFlow)?.sessionId } + }.filterNotNull().first() + } + if (currentSessionId == null) { + Timber.w("Incoming verification request ${data.verificationRequest.details.flowId} discarded, no logged in flow.") + return@launch + } + if (currentSessionId == data.sessionId) { + waitForChildAttached { navTarget -> + navTarget is NavTarget.LoggedInFlow && navTarget.sessionId == currentSessionId + } + .attachSession() + .onIncomingVerificationRequest(data.verificationRequest) + } else { + // Display the request on top of the current session, without switching session + backstack.push(NavTarget.OtherAccountIncomingVerificationRequest(data.sessionId, data.verificationRequest)) + } } private fun switchToNotLoggedInFlow(params: LoginParams?) { @@ -294,6 +366,11 @@ class RootFlowNode( ) : NavTarget @Parcelize data object BugReport : NavTarget + + @Parcelize data class OtherAccountIncomingVerificationRequest( + val sessionId: SessionId, + val verificationRequest: VerificationRequest.Incoming, + ) : NavTarget } override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { @@ -348,6 +425,23 @@ class RootFlowNode( ) } NavTarget.SplashScreen -> emptyNode(buildContext) + is NavTarget.OtherAccountIncomingVerificationRequest -> { + if (matrixSessionCache.getOrNull(navTarget.sessionId) == null) { + Timber.w("Couldn't find session ${navTarget.sessionId} for the incoming verification request") + lifecycleScope.launch { backstack.pop() } + return emptyNode(buildContext) + } + val inputs = OtherSessionIncomingVerificationNode.Inputs( + sessionId = navTarget.sessionId, + verificationRequest = navTarget.verificationRequest, + ) + val callback = object : OtherSessionIncomingVerificationNode.Callback { + override fun onDone() { + backstack.pop() + } + } + createNode(buildContext, plugins = listOf(inputs, callback)) + } NavTarget.BugReport -> { val callback = object : BugReportEntryPoint.Callback { override fun onDone() { diff --git a/appnav/src/main/kotlin/io/element/android/appnav/session/MatrixSessionCache.kt b/appnav/src/main/kotlin/io/element/android/appnav/session/MatrixSessionCache.kt index ffae5f2b17..20d7bad228 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/session/MatrixSessionCache.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/session/MatrixSessionCache.kt @@ -21,6 +21,9 @@ import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.services.analytics.api.AnalyticsService import io.element.android.services.analyticsproviders.api.AnalyticsUserData +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import timber.log.Timber @@ -41,6 +44,12 @@ class MatrixSessionCache( private val analyticsService: AnalyticsService, ) : MatrixClientProvider { private val sessionIdsToMatrixSession = ConcurrentHashMap() + private val _matrixClients = MutableStateFlow>(emptyList()) + + /** + * The [MatrixClient]s currently in the cache. + */ + val matrixClients: StateFlow> = _matrixClients.asStateFlow() private val restoreMutex = Mutex() init { @@ -51,10 +60,12 @@ class MatrixSessionCache( fun removeAll() { sessionIdsToMatrixSession.clear() + updateMatrixClients() } fun remove(sessionId: SessionId) { sessionIdsToMatrixSession.remove(sessionId) + updateMatrixClients() } override fun getOrNull(sessionId: SessionId): MatrixClient? { @@ -122,8 +133,13 @@ class MatrixSessionCache( matrixClient = matrixClient, syncOrchestrator = syncOrchestrator, ) + updateMatrixClients() syncOrchestrator.start() } + + private fun updateMatrixClients() { + _matrixClients.value = sessionIdsToMatrixSession.values.map { it.matrixClient } + } } private data class InMemoryMatrixSession( diff --git a/appnav/src/main/kotlin/io/element/android/appnav/verification/IncomingVerificationRequestObserver.kt b/appnav/src/main/kotlin/io/element/android/appnav/verification/IncomingVerificationRequestObserver.kt new file mode 100644 index 0000000000..bfc952334a --- /dev/null +++ b/appnav/src/main/kotlin/io/element/android/appnav/verification/IncomingVerificationRequestObserver.kt @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.appnav.verification + +import dev.zacsweers.metro.Inject +import io.element.android.appnav.session.MatrixSessionCache +import io.element.android.libraries.core.coroutine.withPreviousValue +import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.matrix.api.verification.SessionVerificationServiceListener +import io.element.android.libraries.matrix.api.verification.VerificationRequest +import io.element.android.services.appnavstate.api.AppForegroundStateService +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.channelFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch +import kotlinx.coroutines.withTimeoutOrNull +import timber.log.Timber +import kotlin.time.Duration.Companion.minutes + +data class IncomingVerificationRequestData( + val sessionId: SessionId, + val verificationRequest: VerificationRequest.Incoming, +) + +/** + * Listens to the incoming verification requests of all the sessions in [MatrixSessionCache], not only the current one. + */ +@Inject +class IncomingVerificationRequestObserver( + private val matrixSessionCache: MatrixSessionCache, + private val appForegroundStateService: AppForegroundStateService, +) { + /** + * Emits the incoming verification requests once the app is in foreground. + * Requests received while the app is in background for too long are discarded. + */ + fun incomingVerificationRequests(): Flow = channelFlow { + launch { + matrixSessionCache.matrixClients.withPreviousValue().collect { (previousMatrixClients, matrixClients) -> + val previousMatrixClientSet = previousMatrixClients.orEmpty().toSet() + // Remove the listener of the clients which are not in the cache anymore + (previousMatrixClientSet - matrixClients.toSet()).forEach { matrixClient -> + matrixClient.sessionVerificationService.setListener(null) + } + (matrixClients - previousMatrixClientSet).forEach { matrixClient -> + val sessionId = matrixClient.sessionId + matrixClient.sessionVerificationService.setListener( + object : SessionVerificationServiceListener { + override fun onIncomingSessionRequest(verificationRequest: VerificationRequest.Incoming) { + launch { + // Wait until the app is in foreground to display the incoming verification request + // TODO there should also be a timeout for > 10 minutes elapsed since the request was created, + // but the SDK doesn't expose that info yet + val isInForeground = withTimeoutOrNull(2.minutes) { + appForegroundStateService.isInForeground.first { it } + } + if (isInForeground == null) { + Timber.w("Incoming verification request ${verificationRequest.details.flowId} discarded due to timeout.") + } else { + send(IncomingVerificationRequestData(sessionId, verificationRequest)) + } + } + } + } + ) + } + } + } + awaitClose { + matrixSessionCache.matrixClients.value.forEach { matrixClient -> + matrixClient.sessionVerificationService.setListener(null) + } + } + } +} diff --git a/appnav/src/main/kotlin/io/element/android/appnav/verification/OtherSessionIncomingVerificationNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/verification/OtherSessionIncomingVerificationNode.kt new file mode 100644 index 0000000000..115c174bb8 --- /dev/null +++ b/appnav/src/main/kotlin/io/element/android/appnav/verification/OtherSessionIncomingVerificationNode.kt @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.appnav.verification + +import android.os.Parcelable +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.bumble.appyx.core.composable.Children +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.navigation.model.permanent.PermanentNavModel +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.node.ParentNode +import com.bumble.appyx.core.plugin.Plugin +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.ContributesNode +import io.element.android.appnav.di.SessionGraphFactory +import io.element.android.features.verifysession.api.IncomingVerificationEntryPoint +import io.element.android.libraries.architecture.NodeInputs +import io.element.android.libraries.architecture.callback +import io.element.android.libraries.architecture.inputs +import io.element.android.libraries.di.DependencyInjectionGraphOwner +import io.element.android.libraries.matrix.api.MatrixClientProvider +import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.matrix.api.verification.VerificationRequest +import kotlinx.parcelize.Parcelize + +/** + * Display an incoming verification request received by a session which is not the current one, without switching the current session. + * It sets up the Session graph of the session receiving the request. + */ +@ContributesNode(AppScope::class) +@AssistedInject +class OtherSessionIncomingVerificationNode( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, + sessionGraphFactory: SessionGraphFactory, + matrixClientProvider: MatrixClientProvider, + private val incomingVerificationEntryPoint: IncomingVerificationEntryPoint, +) : ParentNode( + navModel = PermanentNavModel( + navTargets = setOf(NavTarget), + savedStateMap = buildContext.savedStateMap, + ), + buildContext = buildContext, + plugins = plugins +), DependencyInjectionGraphOwner { + interface Callback : Plugin { + fun onDone() + } + + @Parcelize + object NavTarget : Parcelable + + data class Inputs( + val sessionId: SessionId, + val verificationRequest: VerificationRequest.Incoming, + ) : NodeInputs + + private val callback: Callback = callback() + private val inputs: Inputs = inputs() + override val graph = sessionGraphFactory.create( + requireNotNull(matrixClientProvider.getOrNull(inputs.sessionId)) { "No MatrixClient for session ${inputs.sessionId}" } + ) + + override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { + return incomingVerificationEntryPoint.createNode( + parentNode = this, + buildContext = buildContext, + params = IncomingVerificationEntryPoint.Params(inputs.verificationRequest), + callback = object : IncomingVerificationEntryPoint.Callback { + override fun onDone() { + callback.onDone() + } + }, + ) + } + + @Composable + override fun View(modifier: Modifier) { + Children( + navModel = navModel, + modifier = modifier, + ) + } +} diff --git a/appnav/src/test/kotlin/io/element/android/appnav/RootFlowNodeTest.kt b/appnav/src/test/kotlin/io/element/android/appnav/RootFlowNodeTest.kt index fea30c8019..cc23aea626 100644 --- a/appnav/src/test/kotlin/io/element/android/appnav/RootFlowNodeTest.kt +++ b/appnav/src/test/kotlin/io/element/android/appnav/RootFlowNodeTest.kt @@ -25,6 +25,7 @@ import io.element.android.appnav.root.RootNavStateFlowFactory import io.element.android.appnav.root.RootPresenter import io.element.android.appnav.session.FakeSyncOrchestratorFactory import io.element.android.appnav.session.MatrixSessionCache +import io.element.android.appnav.verification.IncomingVerificationRequestObserver import io.element.android.features.login.api.LoginEntryPoint import io.element.android.features.login.api.LoginParams import io.element.android.features.login.test.FakeLoginEntryPoint @@ -50,6 +51,7 @@ import io.element.android.libraries.sessionstorage.test.InMemorySessionStore import io.element.android.services.analytics.test.FakeAnalyticsService import io.element.android.services.analytics.test.watchers.FakeAnalyticsColdStartWatcher import io.element.android.services.apperror.test.FakeAppErrorStateService +import io.element.android.services.appnavstate.test.FakeAppForegroundStateService import io.element.android.tests.testutils.node.FakeNodeFactoriesBindings import io.element.android.tests.testutils.node.FakeParentNode import io.element.android.tests.testutils.presenter.NotUsedPresenter @@ -183,6 +185,10 @@ class RootFlowNodeTest : RobolectricTest() { announcementService = FakeAnnouncementService(), analyticsService = FakeAnalyticsService(), analyticsColdStartWatcher = FakeAnalyticsColdStartWatcher(), + incomingVerificationRequestObserver = IncomingVerificationRequestObserver( + matrixSessionCache = matrixSessionCache, + appForegroundStateService = FakeAppForegroundStateService(), + ), appCoroutineScope = backgroundScope, ) } diff --git a/appnav/src/test/kotlin/io/element/android/appnav/session/MatrixSessionCacheTest.kt b/appnav/src/test/kotlin/io/element/android/appnav/session/MatrixSessionCacheTest.kt index bf315ddb01..8d4a162d4f 100644 --- a/appnav/src/test/kotlin/io/element/android/appnav/session/MatrixSessionCacheTest.kt +++ b/appnav/src/test/kotlin/io/element/android/appnav/session/MatrixSessionCacheTest.kt @@ -70,9 +70,11 @@ class MatrixSessionCacheTest { fakeAuthenticationService.givenMatrixClient(fakeMatrixClient) assertThat(matrixSessionCache.getOrRestore(A_SESSION_ID).getOrNull()).isEqualTo(fakeMatrixClient) assertThat(matrixSessionCache.getOrNull(A_SESSION_ID)).isEqualTo(fakeMatrixClient) + assertThat(matrixSessionCache.matrixClients.value).containsExactly(fakeMatrixClient) // Remove matrixSessionCache.remove(A_SESSION_ID) assertThat(matrixSessionCache.getOrNull(A_SESSION_ID)).isNull() + assertThat(matrixSessionCache.matrixClients.value).isEmpty() } @Test @@ -83,9 +85,11 @@ class MatrixSessionCacheTest { fakeAuthenticationService.givenMatrixClient(fakeMatrixClient) assertThat(matrixSessionCache.getOrRestore(A_SESSION_ID).getOrNull()).isEqualTo(fakeMatrixClient) assertThat(matrixSessionCache.getOrNull(A_SESSION_ID)).isEqualTo(fakeMatrixClient) + assertThat(matrixSessionCache.matrixClients.value).containsExactly(fakeMatrixClient) // Remove all matrixSessionCache.removeAll() assertThat(matrixSessionCache.getOrNull(A_SESSION_ID)).isNull() + assertThat(matrixSessionCache.matrixClients.value).isEmpty() } @Test diff --git a/appnav/src/test/kotlin/io/element/android/appnav/verification/IncomingVerificationRequestObserverTest.kt b/appnav/src/test/kotlin/io/element/android/appnav/verification/IncomingVerificationRequestObserverTest.kt new file mode 100644 index 0000000000..04522629cd --- /dev/null +++ b/appnav/src/test/kotlin/io/element/android/appnav/verification/IncomingVerificationRequestObserverTest.kt @@ -0,0 +1,167 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.appnav.verification + +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +import io.element.android.appnav.session.MatrixSessionCache +import io.element.android.appnav.session.SyncOrchestrator +import io.element.android.features.networkmonitor.test.FakeNetworkMonitor +import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.api.core.DeviceId +import io.element.android.libraries.matrix.api.core.FlowId +import io.element.android.libraries.matrix.api.user.MatrixUser +import io.element.android.libraries.matrix.api.verification.SessionVerificationRequestDetails +import io.element.android.libraries.matrix.api.verification.VerificationRequest +import io.element.android.libraries.matrix.test.A_SESSION_ID +import io.element.android.libraries.matrix.test.A_SESSION_ID_2 +import io.element.android.libraries.matrix.test.FakeMatrixClient +import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService +import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService +import io.element.android.services.analytics.test.FakeAnalyticsService +import io.element.android.services.appnavstate.test.FakeAppForegroundStateService +import io.element.android.tests.testutils.testCoroutineDispatchers +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.advanceTimeBy +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Test +import kotlin.time.Duration.Companion.minutes +import kotlin.time.Duration.Companion.seconds + +@OptIn(ExperimentalCoroutinesApi::class) +class IncomingVerificationRequestObserverTest { + private val sessionVerificationService1 = FakeSessionVerificationService() + private val sessionVerificationService2 = FakeSessionVerificationService() + + @Test + fun `incoming verification requests of all the sessions are emitted with their session id`() = runTest { + val matrixSessionCache = createMatrixSessionCache() + val observer = IncomingVerificationRequestObserver( + matrixSessionCache = matrixSessionCache, + appForegroundStateService = FakeAppForegroundStateService(), + ) + observer.incomingVerificationRequests().test { + matrixSessionCache.getOrRestore(A_SESSION_ID) + matrixSessionCache.getOrRestore(A_SESSION_ID_2) + runCurrent() + val request = anIncomingVerificationRequest() + sessionVerificationService2.listener!!.onIncomingSessionRequest(request) + assertThat(awaitItem()).isEqualTo(IncomingVerificationRequestData(A_SESSION_ID_2, request)) + sessionVerificationService1.listener!!.onIncomingSessionRequest(request) + assertThat(awaitItem()).isEqualTo(IncomingVerificationRequestData(A_SESSION_ID, request)) + } + // The listeners are removed when the flow is not collected anymore + assertThat(sessionVerificationService1.listener).isNull() + assertThat(sessionVerificationService2.listener).isNull() + } + + @Test + fun `listener is removed when the session is removed from the cache`() = runTest { + val matrixSessionCache = createMatrixSessionCache() + val observer = IncomingVerificationRequestObserver( + matrixSessionCache = matrixSessionCache, + appForegroundStateService = FakeAppForegroundStateService(), + ) + observer.incomingVerificationRequests().test { + matrixSessionCache.getOrRestore(A_SESSION_ID) + matrixSessionCache.getOrRestore(A_SESSION_ID_2) + runCurrent() + assertThat(sessionVerificationService1.listener).isNotNull() + assertThat(sessionVerificationService2.listener).isNotNull() + matrixSessionCache.remove(A_SESSION_ID) + runCurrent() + assertThat(sessionVerificationService1.listener).isNull() + assertThat(sessionVerificationService2.listener).isNotNull() + } + } + + @Test + fun `incoming verification request is emitted when the app goes to foreground`() = runTest { + val matrixSessionCache = createMatrixSessionCache() + val appForegroundStateService = FakeAppForegroundStateService(initialForegroundValue = false) + val observer = IncomingVerificationRequestObserver( + matrixSessionCache = matrixSessionCache, + appForegroundStateService = appForegroundStateService, + ) + observer.incomingVerificationRequests().test { + matrixSessionCache.getOrRestore(A_SESSION_ID) + runCurrent() + val request = anIncomingVerificationRequest() + sessionVerificationService1.listener!!.onIncomingSessionRequest(request) + advanceTimeBy(30.seconds) + expectNoEvents() + appForegroundStateService.givenIsInForeground(true) + assertThat(awaitItem()).isEqualTo(IncomingVerificationRequestData(A_SESSION_ID, request)) + } + } + + @Test + fun `incoming verification request is discarded if the app does not go to foreground in time`() = runTest { + val matrixSessionCache = createMatrixSessionCache() + val appForegroundStateService = FakeAppForegroundStateService(initialForegroundValue = false) + val observer = IncomingVerificationRequestObserver( + matrixSessionCache = matrixSessionCache, + appForegroundStateService = appForegroundStateService, + ) + observer.incomingVerificationRequests().test { + matrixSessionCache.getOrRestore(A_SESSION_ID) + runCurrent() + sessionVerificationService1.listener!!.onIncomingSessionRequest(anIncomingVerificationRequest()) + advanceTimeBy(3.minutes) + appForegroundStateService.givenIsInForeground(true) + runCurrent() + expectNoEvents() + } + } + + private fun TestScope.createMatrixSessionCache(): MatrixSessionCache { + val matrixClient1 = FakeMatrixClient( + sessionId = A_SESSION_ID, + sessionCoroutineScope = backgroundScope, + sessionVerificationService = sessionVerificationService1, + userIdServerNameLambda = { "server" }, + ) + val matrixClient2 = FakeMatrixClient( + sessionId = A_SESSION_ID_2, + sessionCoroutineScope = backgroundScope, + sessionVerificationService = sessionVerificationService2, + userIdServerNameLambda = { "server" }, + ) + return MatrixSessionCache( + authenticationService = FakeMatrixAuthenticationService( + matrixClientResult = { sessionId -> + Result.success(if (sessionId == A_SESSION_ID) matrixClient1 else matrixClient2) + }, + ), + syncOrchestratorFactory = object : SyncOrchestrator.Factory { + override fun create(matrixClient: MatrixClient, sessionCoroutineScope: CoroutineScope) = SyncOrchestrator( + matrixClient = matrixClient, + sessionCoroutineScope = sessionCoroutineScope, + appForegroundStateService = FakeAppForegroundStateService(), + networkMonitor = FakeNetworkMonitor(), + dispatchers = testCoroutineDispatchers(), + analyticsService = FakeAnalyticsService(), + ) + }, + analyticsService = FakeAnalyticsService(), + ) + } + + private fun anIncomingVerificationRequest() = VerificationRequest.Incoming.OtherSession( + details = SessionVerificationRequestDetails( + senderProfile = MatrixUser(A_SESSION_ID), + flowId = FlowId("flowId"), + deviceId = DeviceId("deviceId"), + deviceDisplayName = "a device name", + firstSeenTimestamp = 0, + ) + ) +} diff --git a/features/verifysession/impl/build.gradle.kts b/features/verifysession/impl/build.gradle.kts index 89a87edf19..fc51d87acd 100644 --- a/features/verifysession/impl/build.gradle.kts +++ b/features/verifysession/impl/build.gradle.kts @@ -34,6 +34,7 @@ dependencies { implementation(projects.libraries.matrixui) implementation(projects.libraries.designsystem) implementation(projects.libraries.preferences.api) + implementation(projects.libraries.sessionStorage.api) implementation(projects.libraries.uiStrings) implementation(projects.features.logout.api) api(libs.statemachine) @@ -44,4 +45,5 @@ dependencies { testImplementation(projects.libraries.dateformatter.test) testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.preferences.test) + testImplementation(projects.libraries.sessionStorage.test) } diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationPresenter.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationPresenter.kt index 4cde3f927f..00daf9b5b8 100644 --- a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationPresenter.kt +++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationPresenter.kt @@ -13,6 +13,7 @@ package io.element.android.features.verifysession.impl.incoming import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.remember @@ -26,13 +27,17 @@ import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.dateformatter.api.DateFormatter import io.element.android.libraries.dateformatter.api.DateFormatterMode import io.element.android.libraries.di.annotations.SessionCoroutineScope +import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.api.verification.SessionVerificationRequestDetails import io.element.android.libraries.matrix.api.verification.SessionVerificationService import io.element.android.libraries.matrix.api.verification.VerificationFlowState import io.element.android.libraries.matrix.api.verification.VerificationRequest +import io.element.android.libraries.sessionstorage.api.SessionStore import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import timber.log.Timber @@ -45,6 +50,8 @@ class IncomingVerificationPresenter( @Assisted private val navigator: IncomingVerificationNavigator, @SessionCoroutineScope private val sessionCoroutineScope: CoroutineScope, private val sessionVerificationService: SessionVerificationService, + private val sessionId: SessionId, + private val sessionStore: SessionStore, private val stateMachine: IncomingVerificationStateMachine, private val dateFormatter: DateFormatter, ) : Presenter { @@ -92,9 +99,24 @@ class IncomingVerificationPresenter( mode = DateFormatterMode.TimeOrDate, ) } + val currentUser by remember { + sessionStore.sessionsFlow().mapNotNull { sessions -> + sessions.find { it.userId == sessionId.value }?.let { sessionData -> + MatrixUser( + userId = sessionId, + displayName = sessionData.userDisplayName, + avatarUrl = sessionData.userAvatarUrl, + // Locally stored avatar, used as a fallback when the avatar cannot be loaded, + // for instance for a session which is not the current one + avatarThumbnail = sessionData.userAvatarData, + ) + } + } + }.collectAsState(MatrixUser(sessionId)) val step by remember { derivedStateOf { stateAndDispatch.state.value.toVerificationStep( + currentUser = currentUser, sessionVerificationRequestDetails = verificationRequest.details, formattedSignInTime = formattedSignInTime, ) @@ -147,6 +169,7 @@ class IncomingVerificationPresenter( } private fun StateMachineState?.toVerificationStep( + currentUser: MatrixUser, sessionVerificationRequestDetails: SessionVerificationRequestDetails, formattedSignInTime: String, ): Step = @@ -156,6 +179,7 @@ class IncomingVerificationPresenter( StateMachineState.RejectingIncomingVerification, null -> { Step.Initial( + currentUser = currentUser, deviceDisplayName = sessionVerificationRequestDetails.deviceDisplayName, deviceId = sessionVerificationRequestDetails.deviceId, formattedSignInTime = formattedSignInTime, diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationState.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationState.kt index e3663800b2..bd4e343c49 100644 --- a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationState.kt +++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationState.kt @@ -10,6 +10,7 @@ package io.element.android.features.verifysession.impl.incoming import androidx.compose.runtime.Stable import io.element.android.libraries.matrix.api.core.DeviceId +import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.api.verification.SessionVerificationData import io.element.android.libraries.matrix.api.verification.VerificationRequest @@ -21,6 +22,7 @@ data class IncomingVerificationState( @Stable sealed interface Step { data class Initial( + val currentUser: MatrixUser, val deviceDisplayName: String?, val deviceId: DeviceId, val formattedSignInTime: String, diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationStatePreviewParam.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationStatePreviewParam.kt index 68672be025..bdf3ad2196 100644 --- a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationStatePreviewParam.kt +++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationStatePreviewParam.kt @@ -13,6 +13,7 @@ import io.element.android.features.verifysession.impl.incoming.IncomingVerificat import io.element.android.features.verifysession.impl.ui.aDecimalsSessionVerificationData import io.element.android.features.verifysession.impl.ui.aEmojisSessionVerificationData import io.element.android.libraries.designsystem.preview.USER_NAME_ALICE +import io.element.android.libraries.designsystem.preview.USER_NAME_BOB import io.element.android.libraries.matrix.api.core.DeviceId import io.element.android.libraries.matrix.api.core.FlowId import io.element.android.libraries.matrix.api.core.UserId @@ -50,6 +51,11 @@ open class IncomingVerificationStatePreviewParam : PreviewParameterProvider { _ -> } @@ -59,6 +98,7 @@ class IncomingVerificationPresenterTest { val initialState = awaitItem() assertThat(initialState.step).isEqualTo( IncomingVerificationState.Step.Initial( + currentUser = MatrixUser(A_SESSION_ID), deviceDisplayName = "a device name", deviceId = A_DEVICE_ID, formattedSignInTime = "567 TimeOrDate false", @@ -128,6 +168,7 @@ class IncomingVerificationPresenterTest { val initialState = awaitItem() assertThat(initialState.step).isEqualTo( IncomingVerificationState.Step.Initial( + currentUser = MatrixUser(A_SESSION_ID), deviceDisplayName = "a device name", deviceId = A_DEVICE_ID, formattedSignInTime = "567 TimeOrDate false", @@ -193,6 +234,7 @@ class IncomingVerificationPresenterTest { val initialState = awaitItem() assertThat(initialState.step).isEqualTo( IncomingVerificationState.Step.Initial( + currentUser = MatrixUser(A_SESSION_ID), deviceDisplayName = "a device name", deviceId = A_DEVICE_ID, formattedSignInTime = "567 TimeOrDate false", @@ -228,6 +270,7 @@ class IncomingVerificationPresenterTest { val initialState = awaitItem() assertThat(initialState.step).isEqualTo( IncomingVerificationState.Step.Initial( + currentUser = MatrixUser(A_SESSION_ID), deviceDisplayName = "a device name", deviceId = A_DEVICE_ID, formattedSignInTime = "567 TimeOrDate false", @@ -315,10 +358,13 @@ internal fun TestScope.createPresenter( navigator: IncomingVerificationNavigator = IncomingVerificationNavigator { lambdaError() }, service: SessionVerificationService = FakeSessionVerificationService(), dateFormatter: DateFormatter = FakeDateFormatter(), + sessionStore: SessionStore = InMemorySessionStore(), ) = IncomingVerificationPresenter( verificationRequest = verificationRequest, navigator = navigator, sessionVerificationService = service, + sessionId = A_SESSION_ID, + sessionStore = sessionStore, stateMachine = IncomingVerificationStateMachine(service), dateFormatter = dateFormatter, sessionCoroutineScope = backgroundScope, diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/verification/RustSessionVerificationService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/verification/RustSessionVerificationService.kt index 760937c567..a96d5ec214 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/verification/RustSessionVerificationService.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/verification/RustSessionVerificationService.kt @@ -245,6 +245,7 @@ class RustSessionVerificationService( fun destroy() { Timber.d("Destroying RustSessionVerificationService") + listener = null verificationStateListenerTaskHandle.cancelAndDestroy() recoveryStateListenerTaskHandle.cancelAndDestroy() if (this::verificationController.isInitialized) { diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/accountinfo/AccountInfoCard.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/accountinfo/AccountInfoCard.kt index ab0b172cc5..38de05999f 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/accountinfo/AccountInfoCard.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/accountinfo/AccountInfoCard.kt @@ -127,8 +127,7 @@ private fun DeviceInfoRow( horizontalArrangement = Arrangement.spacedBy(8.dp), ) { TextWithLabelMolecule( - // TODO i18n - label = "Signed in", + label = stringResource(CommonStrings.common_signed_in), text = mode.signInFormattedTimestamp, modifier = Modifier.weight(1f), ) diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/accountinfo/AccountInfoCardState.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/accountinfo/AccountInfoCardState.kt index 68b66d72ee..7077fa7c0b 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/accountinfo/AccountInfoCardState.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/accountinfo/AccountInfoCardState.kt @@ -24,7 +24,6 @@ sealed interface AccountInfoCardMode { val otherUser: MatrixUser, ) : AccountInfoCardMode - // TODO Replace SessionDetailsView with this component data class DeviceVerification( val deviceName: String?, val deviceId: DeviceId, diff --git a/libraries/ui-strings/src/main/res/values/localazy.xml b/libraries/ui-strings/src/main/res/values/localazy.xml index e6ba1d5fc3..102c627d90 100644 --- a/libraries/ui-strings/src/main/res/values/localazy.xml +++ b/libraries/ui-strings/src/main/res/values/localazy.xml @@ -413,6 +413,7 @@ Reason: %1$s." "Shared live location" "Shared location" "Shared space" + "Signed in" "Removing device" "Something went wrong" "We encountered an issue. Please try again." diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming.ui_SessionDetailsView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming.ui_SessionDetailsView_Day_0_en.png deleted file mode 100644 index d56a5c2c1c..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming.ui_SessionDetailsView_Day_0_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ac4f811358248644cd0d83505f28ca248602f26630c8ea7eee9cac4e8a2985c2 -size 24091 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming.ui_SessionDetailsView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming.ui_SessionDetailsView_Night_0_en.png deleted file mode 100644 index a1e8eb4918..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming.ui_SessionDetailsView_Night_0_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:922170fd14008b3df570fd20075e4a26e5b6e2b7ab67722f10fa5e6fc1b5e5a1 -size 23404 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationViewA11y_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationViewA11y_en.png index dd1e161549..af648dfd74 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationViewA11y_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationViewA11y_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:07d112b2a6fbdb4b0b95fa58b4e4bef0a966a9ab8a5be2fe37f28a8282db75bf -size 76657 +oid sha256:44c1e9dab64ce614941750808f002eb2f3510110d39a608fe31d068cec66a756 +size 79926 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_0_en.png index 0223bf0d6d..2cab6515e3 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:06194d9f61128a07129d90e9b82a767561d1cc4ec7b8a700329557d55547e3a7 -size 41988 +oid sha256:51975d611c94b398831339d793e02c76b3dc7967e8eb651b95dd5e38758e38f2 +size 41421 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_1_en.png index 0223bf0d6d..2cab6515e3 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:06194d9f61128a07129d90e9b82a767561d1cc4ec7b8a700329557d55547e3a7 -size 41988 +oid sha256:51975d611c94b398831339d793e02c76b3dc7967e8eb651b95dd5e38758e38f2 +size 41421 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_3_en.png index da9e12181e..528f3545cb 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3cee24c7f31f4062bd64286a5cc9560c551ca53ded85807d6a70531c3592279e -size 41895 +oid sha256:c0c2eacebbb95fa0fd13a1175f447aa7f14907eb84d3ae835fcb7af737bd6d6b +size 41344 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_0_en.png index 36534212e4..8dde8b40ae 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0225f023ac629f7f6dc9697376c47aa80d51fb89b4eebd8e84221c5b3ad3f727 -size 40671 +oid sha256:d0bbc942bf53c563b4952d2c6b8f16f1dc4a1b0e9ed099dd18690e5e1969f131 +size 40249 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_1_en.png index 36534212e4..8dde8b40ae 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0225f023ac629f7f6dc9697376c47aa80d51fb89b4eebd8e84221c5b3ad3f727 -size 40671 +oid sha256:d0bbc942bf53c563b4952d2c6b8f16f1dc4a1b0e9ed099dd18690e5e1969f131 +size 40249 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_3_en.png index 2098df0116..7706d46599 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9035af1399a7a4db027d7610cfc382ae7853a38d8c9027d1b7bcf23f0ec835a1 -size 40493 +oid sha256:e06e5c83e9b82f11e7d8b89c7c9d8af3de04c1becc49ac4c8718147cd7c6d5a7 +size 40054