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 6526248ef8..2e449c2cf9 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt @@ -73,10 +73,13 @@ import io.element.android.libraries.matrix.api.core.RoomIdOrAlias import io.element.android.libraries.matrix.api.core.UserId 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.SessionVerificationRequestDetails 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.AppNavigationStateService import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.MainScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch @@ -84,6 +87,7 @@ import kotlinx.parcelize.Parcelize import timber.log.Timber import java.util.Optional import java.util.UUID +import kotlin.time.Duration.Companion.milliseconds @ContributesNode(SessionScope::class) class LoggedInFlowNode @AssistedInject constructor( @@ -127,8 +131,18 @@ class LoggedInFlowNode @AssistedInject constructor( ) private val verificationListener = object : SessionVerificationServiceListener { - override fun onIncomingSessionRequest(sessionVerificationRequestDetails: SessionVerificationRequestDetails) { - backstack.singleTop(NavTarget.IncomingVerificationRequest(sessionVerificationRequestDetails)) + 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. + MainScope().launch { + // Wait until the app is in foreground to display the incoming verification request + appNavigationStateService.appNavigationState.first { it.isInForeground } + + // Wait for the UI to be ready + delay(500.milliseconds) + + backstack.singleTop(NavTarget.IncomingVerificationRequest(verificationRequest)) + } } } @@ -218,7 +232,7 @@ class LoggedInFlowNode @AssistedInject constructor( data object LogoutForNativeSlidingSyncMigrationNeeded : NavTarget @Parcelize - data class IncomingVerificationRequest(val data: SessionVerificationRequestDetails) : NavTarget + data class IncomingVerificationRequest(val data: VerificationRequest.Incoming) : NavTarget } override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { diff --git a/features/ftue/impl/build.gradle.kts b/features/ftue/impl/build.gradle.kts index d9cd993043..652f989acb 100644 --- a/features/ftue/impl/build.gradle.kts +++ b/features/ftue/impl/build.gradle.kts @@ -1,4 +1,5 @@ import extension.setupAnvil +import org.gradle.kotlin.dsl.test /* * Copyright 2023, 2024 New Vector Ltd. @@ -14,6 +15,12 @@ plugins { android { namespace = "io.element.android.features.ftue.impl" + + testOptions { + unitTests { + isIncludeAndroidResources = true + } + } } setupAnvil() @@ -30,6 +37,7 @@ dependencies { implementation(projects.libraries.uiStrings) implementation(projects.libraries.testtags) implementation(projects.features.analytics.api) + implementation(projects.features.logout.api) implementation(projects.features.securebackup.api) implementation(projects.features.verifysession.api) implementation(projects.services.analytics.api) @@ -37,12 +45,16 @@ dependencies { implementation(projects.libraries.permissions.api) implementation(projects.libraries.permissions.noop) implementation(projects.services.toolbox.api) + implementation(projects.appconfig) testImplementation(libs.test.junit) testImplementation(libs.coroutines.test) testImplementation(libs.molecule.runtime) testImplementation(libs.test.truth) testImplementation(libs.test.turbine) + testImplementation(libs.test.robolectric) + testImplementation(libs.androidx.compose.ui.test.junit) + testReleaseImplementation(libs.androidx.compose.ui.test.manifest) testImplementation(projects.libraries.matrix.test) testImplementation(projects.services.analytics.test) testImplementation(projects.services.analytics.noop) diff --git a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/FtueFlowNode.kt b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/FtueFlowNode.kt index c5987508fe..034701fec4 100644 --- a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/FtueFlowNode.kt +++ b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/FtueFlowNode.kt @@ -16,7 +16,6 @@ import androidx.compose.ui.Modifier import androidx.lifecycle.lifecycleScope import com.bumble.appyx.core.lifecycle.subscribe import com.bumble.appyx.core.modality.BuildContext -import com.bumble.appyx.core.navigation.backpresshandlerstrategies.BaseBackPressHandlerStrategy import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.plugin.Plugin import com.bumble.appyx.navmodel.backstack.BackStack @@ -38,8 +37,6 @@ import io.element.android.libraries.designsystem.theme.components.CircularProgre import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.SessionScope import io.element.android.services.analytics.api.AnalyticsService -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.launchIn @@ -59,7 +56,6 @@ class FtueFlowNode @AssistedInject constructor( backstack = BackStack( initialElement = NavTarget.Placeholder, savedStateMap = buildContext.savedStateMap, - backPressHandler = NoOpBackstackHandlerStrategy(), ), buildContext = buildContext, plugins = plugins, @@ -104,7 +100,7 @@ class FtueFlowNode @AssistedInject constructor( NavTarget.Placeholder -> { createNode(buildContext) } - NavTarget.SessionVerification -> { + is NavTarget.SessionVerification -> { val callback = object : FtueSessionVerificationFlowNode.Callback { override fun onDone() { moveToNextStepIfNeeded() @@ -175,11 +171,3 @@ class FtueFlowNode @AssistedInject constructor( } } } - -private class NoOpBackstackHandlerStrategy : BaseBackPressHandlerStrategy() { - override val canHandleBackPressFlow: StateFlow = MutableStateFlow(true) - - override fun onBackPressed() { - // No-op - } -} diff --git a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/di/FtueModule.kt b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/di/FtueModule.kt new file mode 100644 index 0000000000..4387b3dc00 --- /dev/null +++ b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/di/FtueModule.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.ftue.impl.di + +import com.squareup.anvil.annotations.ContributesTo +import dagger.Binds +import dagger.Module +import io.element.android.features.ftue.impl.sessionverification.choosemode.ChooseSelfVerificationModePresenter +import io.element.android.features.ftue.impl.sessionverification.choosemode.ChooseSelfVerificationModeState +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.di.SessionScope + +@ContributesTo(SessionScope::class) +@Module +interface FtueModule { + @Binds + fun bindChooseSelfVerificationMethodPresenter(presenter: ChooseSelfVerificationModePresenter): Presenter +} diff --git a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/FtueSessionVerificationFlowNode.kt b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/FtueSessionVerificationFlowNode.kt index 4ff7e393b4..eeffbdfe50 100644 --- a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/FtueSessionVerificationFlowNode.kt +++ b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/FtueSessionVerificationFlowNode.kt @@ -9,6 +9,7 @@ package io.element.android.features.ftue.impl.sessionverification import android.os.Parcelable import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.Modifier import androidx.lifecycle.lifecycleScope import com.bumble.appyx.core.modality.BuildContext @@ -17,15 +18,21 @@ import com.bumble.appyx.core.plugin.Plugin import com.bumble.appyx.core.plugin.plugins import com.bumble.appyx.navmodel.backstack.BackStack import com.bumble.appyx.navmodel.backstack.operation.newRoot +import com.bumble.appyx.navmodel.backstack.operation.pop import com.bumble.appyx.navmodel.backstack.operation.push import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode +import io.element.android.appconfig.LearnMoreConfig +import io.element.android.features.ftue.impl.sessionverification.choosemode.ChooseSelfVerificationModeNode import io.element.android.features.securebackup.api.SecureBackupEntryPoint import io.element.android.features.verifysession.api.VerifySessionEntryPoint import io.element.android.libraries.architecture.BackstackView import io.element.android.libraries.architecture.BaseFlowNode +import io.element.android.libraries.architecture.createNode +import io.element.android.libraries.designsystem.utils.OpenUrlInTabView import io.element.android.libraries.di.SessionScope +import io.element.android.libraries.matrix.api.verification.VerificationRequest import kotlinx.coroutines.launch import kotlinx.parcelize.Parcelize @@ -37,7 +44,7 @@ class FtueSessionVerificationFlowNode @AssistedInject constructor( private val secureBackupEntryPoint: SecureBackupEntryPoint, ) : BaseFlowNode( backstack = BackStack( - initialElement = NavTarget.Root(showDeviceVerifiedScreen = false), + initialElement = NavTarget.Root, savedStateMap = buildContext.savedStateMap, ), buildContext = buildContext, @@ -45,7 +52,10 @@ class FtueSessionVerificationFlowNode @AssistedInject constructor( ) { sealed interface NavTarget : Parcelable { @Parcelize - data class Root(val showDeviceVerifiedScreen: Boolean) : NavTarget + data object Root : NavTarget + + @Parcelize + data object UseAnotherDevice : NavTarget @Parcelize data object EnterRecoveryKey : NavTarget @@ -62,7 +72,7 @@ class FtueSessionVerificationFlowNode @AssistedInject constructor( override fun onDone() { lifecycleScope.launch { // Move to the completed state view in the verification flow - backstack.newRoot(NavTarget.Root(showDeviceVerifiedScreen = true)) + backstack.newRoot(NavTarget.UseAnotherDevice) } } } @@ -70,19 +80,43 @@ class FtueSessionVerificationFlowNode @AssistedInject constructor( override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { return when (navTarget) { is NavTarget.Root -> { - verifySessionEntryPoint.nodeBuilder(this, buildContext) - .params(VerifySessionEntryPoint.Params(navTarget.showDeviceVerifiedScreen)) - .callback(object : VerifySessionEntryPoint.Callback { - override fun onEnterRecoveryKey() { - backstack.push(NavTarget.EnterRecoveryKey) - } + val callback = object : ChooseSelfVerificationModeNode.Callback { + override fun onUseAnotherDevice() { + backstack.push(NavTarget.UseAnotherDevice) + } + override fun onUseRecoveryKey() { + backstack.push(NavTarget.EnterRecoveryKey) + } + + override fun onResetKey() { + backstack.push(NavTarget.ResetIdentity) + } + + override fun onLearnMoreAboutEncryption() { + learnMoreUrl.value = LearnMoreConfig.ENCRYPTION_URL + } + } + + createNode(buildContext, plugins = listOf(callback)) + } + is NavTarget.UseAnotherDevice -> { + verifySessionEntryPoint.nodeBuilder(this, buildContext) + .params(VerifySessionEntryPoint.Params( + showDeviceVerifiedScreen = true, + verificationRequest = VerificationRequest.Outgoing.CurrentSession, + )) + .callback(object : VerifySessionEntryPoint.Callback { override fun onDone() { plugins().forEach { it.onDone() } } - override fun onResetKey() { - backstack.push(NavTarget.ResetIdentity) + override fun onBack() { + backstack.pop() + } + + override fun onLearnMoreAboutEncryption() { + learnMoreUrl.value = LearnMoreConfig.ENCRYPTION_URL } }) .build() @@ -106,8 +140,12 @@ class FtueSessionVerificationFlowNode @AssistedInject constructor( } } + private val learnMoreUrl = mutableStateOf(null) + @Composable override fun View(modifier: Modifier) { BackstackView() + + OpenUrlInTabView(learnMoreUrl) } } diff --git a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSelfVerificationModeEvent.kt b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSelfVerificationModeEvent.kt new file mode 100644 index 0000000000..99e5950693 --- /dev/null +++ b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSelfVerificationModeEvent.kt @@ -0,0 +1,12 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.ftue.impl.sessionverification.choosemode + +sealed interface ChooseSelfVerificationModeEvent { + data object SignOut : ChooseSelfVerificationModeEvent +} diff --git a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSelfVerificationModeNode.kt b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSelfVerificationModeNode.kt new file mode 100644 index 0000000000..687dc4aefe --- /dev/null +++ b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSelfVerificationModeNode.kt @@ -0,0 +1,54 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.ftue.impl.sessionverification.choosemode + +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 com.bumble.appyx.core.plugin.plugins +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import io.element.android.anvilannotations.ContributesNode +import io.element.android.features.logout.api.direct.DirectLogoutView +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.di.SessionScope + +@ContributesNode(SessionScope::class) +class ChooseSelfVerificationModeNode @AssistedInject constructor( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, + private val presenter: Presenter, + private val directLogoutView: DirectLogoutView, +) : Node(buildContext, plugins = plugins) { + interface Callback : Plugin { + fun onUseAnotherDevice() + fun onUseRecoveryKey() + fun onResetKey() + fun onLearnMoreAboutEncryption() + } + + private val callback = plugins().first() + + @Composable + override fun View(modifier: Modifier) { + val state = presenter.present() + + ChooseSelfVerificationModeView( + state = state, + onUseAnotherDevice = callback::onUseAnotherDevice, + onUseRecoveryKey = callback::onUseRecoveryKey, + onResetKey = callback::onResetKey, + onLearnMore = callback::onLearnMoreAboutEncryption, + modifier = modifier, + ) + + directLogoutView.Render(state = state.directLogoutState) + } +} diff --git a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSelfVerificationModePresenter.kt b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSelfVerificationModePresenter.kt new file mode 100644 index 0000000000..0aec3b4e1d --- /dev/null +++ b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSelfVerificationModePresenter.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.ftue.impl.sessionverification.choosemode + +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 io.element.android.features.logout.api.direct.DirectLogoutEvents +import io.element.android.features.logout.api.direct.DirectLogoutState +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.matrix.api.encryption.EncryptionService +import io.element.android.libraries.matrix.api.encryption.RecoveryState +import javax.inject.Inject + +class ChooseSelfVerificationModePresenter @Inject constructor( + private val encryptionService: EncryptionService, + private val directLogoutPresenter: Presenter, +) : Presenter { + @Composable + override fun present(): ChooseSelfVerificationModeState { + val isLastDevice by encryptionService.isLastDevice.collectAsState() + val recoveryState by encryptionService.recoveryStateStateFlow.collectAsState() + val canEnterRecoveryKey by remember { derivedStateOf { recoveryState == RecoveryState.INCOMPLETE } } + + val directLogoutState = directLogoutPresenter.present() + + fun eventHandler(event: ChooseSelfVerificationModeEvent) { + when (event) { + ChooseSelfVerificationModeEvent.SignOut -> directLogoutState.eventSink(DirectLogoutEvents.Logout(ignoreSdkError = false)) + } + } + + return ChooseSelfVerificationModeState( + isLastDevice = isLastDevice, + canEnterRecoveryKey = canEnterRecoveryKey, + directLogoutState = directLogoutState, + eventSink = ::eventHandler, + ) + } +} diff --git a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSelfVerificationModeState.kt b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSelfVerificationModeState.kt new file mode 100644 index 0000000000..21c37a4ae2 --- /dev/null +++ b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSelfVerificationModeState.kt @@ -0,0 +1,17 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.ftue.impl.sessionverification.choosemode + +import io.element.android.features.logout.api.direct.DirectLogoutState + +data class ChooseSelfVerificationModeState( + val isLastDevice: Boolean, + val canEnterRecoveryKey: Boolean, + val directLogoutState: DirectLogoutState, + val eventSink: (ChooseSelfVerificationModeEvent) -> Unit, +) diff --git a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSelfVerificationModeStateProvider.kt b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSelfVerificationModeStateProvider.kt new file mode 100644 index 0000000000..574aa367c6 --- /dev/null +++ b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSelfVerificationModeStateProvider.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.ftue.impl.sessionverification.choosemode + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.features.logout.api.direct.aDirectLogoutState + +class ChooseSelfVerificationModeStateProvider : + PreviewParameterProvider { + override val values = sequenceOf( + aChooseSelfVerificationModeState(isLastDevice = true, canEnterRecoveryKey = true), + aChooseSelfVerificationModeState(isLastDevice = true, canEnterRecoveryKey = false), + aChooseSelfVerificationModeState(isLastDevice = false, canEnterRecoveryKey = true), + aChooseSelfVerificationModeState(isLastDevice = false, canEnterRecoveryKey = false), + ) +} + +fun aChooseSelfVerificationModeState( + isLastDevice: Boolean = false, + canEnterRecoveryKey: Boolean = true, +) = ChooseSelfVerificationModeState( + isLastDevice = isLastDevice, + canEnterRecoveryKey = canEnterRecoveryKey, + directLogoutState = aDirectLogoutState(), + eventSink = {}, +) diff --git a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSelfVerificationModeView.kt b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSelfVerificationModeView.kt new file mode 100644 index 0000000000..9839f3bf7c --- /dev/null +++ b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSelfVerificationModeView.kt @@ -0,0 +1,127 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.ftue.impl.sessionverification.choosemode + +import androidx.activity.compose.BackHandler +import androidx.activity.compose.LocalActivity +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.runtime.Composable +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.compound.theme.ElementTheme +import io.element.android.compound.tokens.generated.CompoundIcons +import io.element.android.features.ftue.impl.R +import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule +import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage +import io.element.android.libraries.designsystem.components.BigIcon +import io.element.android.libraries.designsystem.components.PageTitle +import io.element.android.libraries.designsystem.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.OutlinedButton +import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.designsystem.theme.components.TextButton +import io.element.android.libraries.designsystem.theme.components.TopAppBar +import io.element.android.libraries.ui.strings.CommonStrings + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ChooseSelfVerificationModeView( + state: ChooseSelfVerificationModeState, + onUseAnotherDevice: () -> Unit, + onUseRecoveryKey: () -> Unit, + onResetKey: () -> Unit, + onLearnMore: () -> Unit, + modifier: Modifier = Modifier +) { + val activity = LocalActivity.current + BackHandler { + activity?.finish() + } + + HeaderFooterPage( + modifier = modifier, + topBar = { + TopAppBar( + title = {}, + actions = { + TextButton( + text = stringResource(CommonStrings.action_signout), + onClick = { state.eventSink(ChooseSelfVerificationModeEvent.SignOut) } + ) + } + ) + }, + header = { + PageTitle( + iconStyle = BigIcon.Style.Default(CompoundIcons.LockSolid()), + title = stringResource(id = R.string.screen_identity_confirmation_title), + subtitle = stringResource(id = R.string.screen_identity_confirmation_subtitle) + ) + }, + footer = { + ButtonColumnMolecule( + modifier = Modifier.padding(bottom = 16.dp) + ) { + if (state.isLastDevice.not()) { + Button( + modifier = Modifier.fillMaxWidth(), + text = stringResource(R.string.screen_identity_use_another_device), + onClick = onUseAnotherDevice, + ) + } + if (state.canEnterRecoveryKey) { + Button( + modifier = Modifier.fillMaxWidth(), + text = stringResource(R.string.screen_session_verification_enter_recovery_key), + onClick = onUseRecoveryKey, + ) + } + OutlinedButton( + modifier = Modifier.fillMaxWidth(), + text = stringResource(R.string.screen_identity_confirmation_cannot_confirm), + onClick = onResetKey, + ) + } + } + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Center, + ) { + Text( + modifier = Modifier + .clickable(onClick = onLearnMore) + .padding(vertical = 4.dp, horizontal = 16.dp), + text = stringResource(CommonStrings.action_learn_more), + style = ElementTheme.typography.fontBodyLgMedium + ) + } + } +} + +@PreviewsDayNight +@Composable +internal fun ChooseSelfVerificationModeViewPreview( + @PreviewParameter(ChooseSelfVerificationModeStateProvider::class) state: ChooseSelfVerificationModeState +) = ElementPreview { + ChooseSelfVerificationModeView( + state = state, + onUseAnotherDevice = {}, + onUseRecoveryKey = {}, + onResetKey = {}, + onLearnMore = {}, + ) +} diff --git a/features/ftue/impl/src/main/res/values/localazy.xml b/features/ftue/impl/src/main/res/values/localazy.xml index 3e8c86b761..47214a9304 100644 --- a/features/ftue/impl/src/main/res/values/localazy.xml +++ b/features/ftue/impl/src/main/res/values/localazy.xml @@ -1,7 +1,18 @@ + "Can\'t confirm?" + "Create a new recovery key" + "Verify this device to set up secure messaging." + "Confirm your identity" + "Use another device" + "Use recovery key" + "Now you can read or send messages securely, and anyone you chat with can also trust this device." + "Device verified" + "Use another device" + "Waiting on other device…" "You can change your settings later." "Allow notifications and never miss a message" + "Enter recovery key" "Calls, polls, search and more will be added later this year." "Message history for encrypted rooms isn’t available yet." "We’d love to hear from you, let us know what you think via the settings page." diff --git a/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSessionVerificationModePresenterTest.kt b/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSessionVerificationModePresenterTest.kt new file mode 100644 index 0000000000..3801001ed4 --- /dev/null +++ b/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSessionVerificationModePresenterTest.kt @@ -0,0 +1,64 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.ftue.impl.sessionverification.choosemode + +import com.google.common.truth.Truth.assertThat +import io.element.android.features.logout.api.direct.DirectLogoutEvents +import io.element.android.features.logout.api.direct.DirectLogoutState +import io.element.android.features.logout.api.direct.aDirectLogoutState +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.matrix.api.encryption.RecoveryState +import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService +import io.element.android.tests.testutils.lambda.lambdaRecorder +import io.element.android.tests.testutils.lambda.value +import io.element.android.tests.testutils.test +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class ChooseSessionVerificationModePresenterTest { + @Test + fun `initial state - is relayed from EncryptionService`() = runTest { + val encryptionService = FakeEncryptionService().apply { + // Is last device + emitIsLastDevice(true) + // Can enter recovery key + emitRecoveryState(RecoveryState.INCOMPLETE) + } + val presenter = createPresenter(encryptionService = encryptionService) + presenter.test { + awaitItem().run { + assertThat(isLastDevice).isTrue() + assertThat(canEnterRecoveryKey).isTrue() + assertThat(directLogoutState.logoutAction.isUninitialized()).isTrue() + } + } + } + + @Test + fun `sing out action triggers a direct logout`() = runTest { + val logoutEventRecorder = lambdaRecorder {} + val logoutPresenter = Presenter { + aDirectLogoutState(eventSink = logoutEventRecorder) + } + val presenter = createPresenter(directLogoutPresenter = logoutPresenter) + presenter.test { + val initial = awaitItem() + initial.eventSink(ChooseSelfVerificationModeEvent.SignOut) + + logoutEventRecorder.assertions().isCalledOnce().with(value(DirectLogoutEvents.Logout(ignoreSdkError = false))) + } + } + + private fun createPresenter( + encryptionService: FakeEncryptionService = FakeEncryptionService(), + directLogoutPresenter: Presenter = Presenter { aDirectLogoutState() } + ) = ChooseSelfVerificationModePresenter( + encryptionService = encryptionService, + directLogoutPresenter = directLogoutPresenter, + ) +} diff --git a/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSessionVerificationModeViewTest.kt b/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSessionVerificationModeViewTest.kt new file mode 100644 index 0000000000..b89e3e42bd --- /dev/null +++ b/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSessionVerificationModeViewTest.kt @@ -0,0 +1,94 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.ftue.impl.sessionverification.choosemode + +import androidx.activity.ComponentActivity +import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.element.android.features.ftue.impl.R +import io.element.android.libraries.ui.strings.CommonStrings +import io.element.android.tests.testutils.EnsureNeverCalled +import io.element.android.tests.testutils.clickOn +import io.element.android.tests.testutils.ensureCalledOnce +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TestRule +import org.junit.runner.RunWith +import org.robolectric.annotation.Config + +@RunWith(AndroidJUnit4::class) +class ChooseSessionVerificationModeViewTest { + @get:Rule val rule = createAndroidComposeRule() + + @Config(qualifiers = "h1024dp") + @Test + fun `clicking on learn more invokes the expected callback`() { + ensureCalledOnce { callback -> + rule.setChooseSelfVerificationModeView( + aChooseSelfVerificationModeState(), + onLearnMoreClick = callback, + ) + rule.clickOn(CommonStrings.action_learn_more) + } + } + + @Config(qualifiers = "h1024dp") + @Test + fun `clicking on use another device calls the callback`() { + ensureCalledOnce { callback -> + rule.setChooseSelfVerificationModeView( + aChooseSelfVerificationModeState(isLastDevice = false), + onUseAnotherDevice = callback, + ) + rule.clickOn(R.string.screen_identity_use_another_device) + } + } + + @Config(qualifiers = "h1024dp") + @Test + fun `clicking on enter recovery key calls the callback`() { + ensureCalledOnce { callback -> + rule.setChooseSelfVerificationModeView( + aChooseSelfVerificationModeState(canEnterRecoveryKey = true), + onEnterRecoveryKey = callback, + ) + rule.clickOn(R.string.screen_session_verification_enter_recovery_key) + } + } + + @Config(qualifiers = "h1024dp") + @Test + fun `clicking on cannot confirm calls the reset keys callback`() { + ensureCalledOnce { callback -> + rule.setChooseSelfVerificationModeView( + aChooseSelfVerificationModeState(), + onResetKey = callback, + ) + rule.clickOn(R.string.screen_identity_confirmation_cannot_confirm) + } + } + + private fun AndroidComposeTestRule.setChooseSelfVerificationModeView( + state: ChooseSelfVerificationModeState, + onLearnMoreClick: () -> Unit = EnsureNeverCalled(), + onUseAnotherDevice: () -> Unit = EnsureNeverCalled(), + onResetKey: () -> Unit = EnsureNeverCalled(), + onEnterRecoveryKey: () -> Unit = EnsureNeverCalled(), + ) { + setContent { + ChooseSelfVerificationModeView( + state = state, + onLearnMore = onLearnMoreClick, + onUseAnotherDevice = onUseAnotherDevice, + onResetKey = onResetKey, + onUseRecoveryKey = onEnterRecoveryKey, + ) + } + } +} diff --git a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/ui/LogoutActionDialog.kt b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/ui/LogoutActionDialog.kt index 7249871772..4b8e23ab4e 100644 --- a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/ui/LogoutActionDialog.kt +++ b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/ui/LogoutActionDialog.kt @@ -8,7 +8,6 @@ package io.element.android.features.logout.impl.ui import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue import androidx.compose.ui.res.stringResource import io.element.android.features.logout.impl.R import io.element.android.libraries.architecture.AsyncAction diff --git a/features/roomdetails/impl/build.gradle.kts b/features/roomdetails/impl/build.gradle.kts index 1c092ec805..315b00b153 100644 --- a/features/roomdetails/impl/build.gradle.kts +++ b/features/roomdetails/impl/build.gradle.kts @@ -24,6 +24,7 @@ android { setupAnvil() dependencies { + implementation(projects.appconfig) implementation(projects.libraries.core) implementation(projects.libraries.architecture) implementation(projects.libraries.matrix.api) @@ -51,6 +52,7 @@ dependencies { implementation(projects.features.messages.api) implementation(projects.features.roomcall.api) implementation(projects.features.knockrequests.api) + implementation(projects.features.verifysession.api) testImplementation(libs.test.junit) testImplementation(libs.coroutines.test) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt index 774abfe8cd..b7cc8685e4 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt @@ -9,6 +9,7 @@ package io.element.android.features.roomdetails.impl import android.os.Parcelable import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.Modifier import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node @@ -21,6 +22,7 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedInject import im.vector.app.features.analytics.plan.Interaction import io.element.android.anvilannotations.ContributesNode +import io.element.android.appconfig.LearnMoreConfig import io.element.android.features.call.api.CallType import io.element.android.features.call.api.ElementCallEntryPoint import io.element.android.features.knockrequests.api.list.KnockRequestsListEntryPoint @@ -35,11 +37,13 @@ import io.element.android.features.roomdetails.impl.notificationsettings.RoomNot import io.element.android.features.roomdetails.impl.rolesandpermissions.RolesAndPermissionsFlowNode import io.element.android.features.roomdetails.impl.securityandprivacy.SecurityAndPrivacyFlowNode import io.element.android.features.userprofile.shared.UserProfileNodeHelper +import io.element.android.features.verifysession.api.VerifySessionEntryPoint import io.element.android.libraries.architecture.BackstackWithOverlayBox import io.element.android.libraries.architecture.BaseFlowNode import io.element.android.libraries.architecture.createNode import io.element.android.libraries.architecture.overlay.operation.hide import io.element.android.libraries.architecture.overlay.operation.show +import io.element.android.libraries.designsystem.utils.OpenUrlInTabView import io.element.android.libraries.di.RoomScope import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.RoomId @@ -47,6 +51,7 @@ import io.element.android.libraries.matrix.api.core.UserId 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.MatrixRoom +import io.element.android.libraries.matrix.api.verification.VerificationRequest import io.element.android.libraries.mediaviewer.api.MediaGalleryEntryPoint import io.element.android.libraries.mediaviewer.api.MediaViewerEntryPoint import io.element.android.services.analytics.api.AnalyticsService @@ -65,6 +70,7 @@ class RoomDetailsFlowNode @AssistedInject constructor( private val knockRequestsListEntryPoint: KnockRequestsListEntryPoint, private val mediaViewerEntryPoint: MediaViewerEntryPoint, private val mediaGalleryEntryPoint: MediaGalleryEntryPoint, + private val verifySessionEntryPoint: VerifySessionEntryPoint, ) : BaseFlowNode( backstack = BackStack( initialElement = plugins.filterIsInstance().first().initialElement.toNavTarget(), @@ -118,6 +124,9 @@ class RoomDetailsFlowNode @AssistedInject constructor( @Parcelize data object SecurityAndPrivacy : NavTarget + + @Parcelize + data class VerifyUser(val userId: UserId) : NavTarget } override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { @@ -168,6 +177,10 @@ class RoomDetailsFlowNode @AssistedInject constructor( backstack.push(NavTarget.SecurityAndPrivacy) } + override fun openDmUserProfile(userId: UserId) { + backstack.push(NavTarget.RoomMemberDetails(userId)) + } + override fun onJoinCall() { val inputs = CallType.RoomCall( sessionId = room.sessionId, @@ -224,6 +237,10 @@ class RoomDetailsFlowNode @AssistedInject constructor( override fun onStartCall(dmRoomId: RoomId) { elementCallEntryPoint.startCall(CallType.RoomCall(roomId = dmRoomId, sessionId = room.sessionId)) } + + override fun onVerifyUser(userId: UserId) { + backstack.push(NavTarget.VerifyUser(userId)) + } } val plugins = listOf(RoomMemberDetailsNode.RoomMemberDetailsInput(navTarget.roomMemberId), callback) createNode(buildContext, plugins) @@ -301,11 +318,37 @@ class RoomDetailsFlowNode @AssistedInject constructor( NavTarget.SecurityAndPrivacy -> { createNode(buildContext) } + is NavTarget.VerifyUser -> { + val params = VerifySessionEntryPoint.Params( + showDeviceVerifiedScreen = true, + verificationRequest = VerificationRequest.Outgoing.User(userId = navTarget.userId,) + ) + verifySessionEntryPoint.nodeBuilder(this, buildContext) + .params(params) + .callback(object : VerifySessionEntryPoint.Callback { + override fun onDone() { + backstack.pop() + } + + override fun onBack() { + backstack.pop() + } + + override fun onLearnMoreAboutEncryption() { + learnMoreUrl.value = LearnMoreConfig.ENCRYPTION_URL + } + }) + .build() + } } } + private val learnMoreUrl = mutableStateOf(null) + @Composable override fun View(modifier: Modifier) { BackstackWithOverlayBox(modifier) + + OpenUrlInTabView(learnMoreUrl) } } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsNode.kt index 6ae3c55503..c3a0a3d519 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsNode.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsNode.kt @@ -23,6 +23,7 @@ import im.vector.app.features.analytics.plan.MobileScreen import io.element.android.anvilannotations.ContributesNode import io.element.android.libraries.androidutils.system.startSharePlainTextIntent import io.element.android.libraries.di.RoomScope +import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.services.analytics.api.AnalyticsService import kotlinx.coroutines.CoroutineScope @@ -50,6 +51,7 @@ class RoomDetailsNode @AssistedInject constructor( fun openPinnedMessagesList() fun openKnockRequestsList() fun openSecurityAndPrivacy() + fun openDmUserProfile(userId: UserId) fun onJoinCall() } @@ -126,6 +128,10 @@ class RoomDetailsNode @AssistedInject constructor( callbacks.forEach { it.openSecurityAndPrivacy() } } + private fun onProfileClick(userId: UserId) { + callbacks.forEach { it.openDmUserProfile(userId) } + } + @Composable override fun View(modifier: Modifier) { val context = LocalContext.current @@ -158,7 +164,8 @@ class RoomDetailsNode @AssistedInject constructor( onJoinCallClick = ::onJoinCall, onPinnedMessagesClick = ::openPinnedMessages, onKnockRequestsClick = ::openKnockRequestsLists, - onSecurityAndPrivacyClick = ::openSecurityAndPrivacy + onSecurityAndPrivacyClick = ::openSecurityAndPrivacy, + onProfileClick = ::onProfileClick, ) } } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt index c1e6a84617..10abb2e86a 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt @@ -70,6 +70,7 @@ import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.TopAppBar import io.element.android.libraries.matrix.api.core.RoomAlias import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.api.room.RoomNotificationMode import io.element.android.libraries.matrix.api.room.getBestName @@ -101,6 +102,7 @@ fun RoomDetailsView( onPinnedMessagesClick: () -> Unit, onKnockRequestsClick: () -> Unit, onSecurityAndPrivacyClick: () -> Unit, + onProfileClick: (UserId) -> Unit, modifier: Modifier = Modifier, ) { Scaffold( @@ -179,11 +181,16 @@ fun RoomDetailsView( state.eventSink(RoomDetailsEvent.SetFavorite(it)) } ) + if (state.canShowSecurityAndPrivacy) { SecurityAndPrivacyItem( onClick = onSecurityAndPrivacyClick ) } + + state.roomMemberDetailsState?.let { dmMemberDetails -> + ProfileItem(onClick = { onProfileClick(dmMemberDetails.userId) }) + } } if (state.roomType is RoomDetailsType.Room) { @@ -546,6 +553,17 @@ private fun FavoriteItem( ) } +@Composable +private fun ProfileItem( + onClick: () -> Unit, +) { + ListItem( + leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.UserProfile())), + headlineContent = { Text(stringResource(id = R.string.screen_room_details_profile_row_title)) }, + onClick = onClick, + ) +} + @Composable private fun MembersItem( memberCount: Long, @@ -655,5 +673,6 @@ private fun ContentToPreview(state: RoomDetailsState) { onPinnedMessagesClick = {}, onKnockRequestsClick = {}, onSecurityAndPrivacyClick = {}, + onProfileClick = {}, ) } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsNode.kt index 7b5ce77f2b..28698b9451 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsNode.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsNode.kt @@ -79,6 +79,7 @@ class RoomMemberDetailsNode @AssistedInject constructor( onOpenDm = ::onStartDM, onStartCall = ::onStartCall, openAvatarPreview = callback::openAvatarPreview, + onVerifyClick = callback::onVerifyUser, ) } } diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsViewTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsViewTest.kt index 8cc1af1638..da7f92aed5 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsViewTest.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsViewTest.kt @@ -15,7 +15,10 @@ import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.performClick import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.features.roomdetails.impl.members.aRoomMember +import io.element.android.features.userprofile.shared.aUserProfileState +import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.room.RoomNotificationMode +import io.element.android.libraries.matrix.test.A_USER_ID import io.element.android.libraries.testtags.TestTags import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.tests.testutils.EnsureCalledOnceWithTwoParams @@ -294,6 +297,21 @@ class RoomDetailsViewTest { rule.clickOn(R.string.screen_room_details_requests_to_join_title) } } + + @Config(qualifiers = "h1024dp") + @Test + fun `click on profile invokes the expected callback`() { + ensureCalledOnceWithParam(A_USER_ID) { callback -> + rule.setRoomDetailView( + state = aRoomDetailsState( + eventSink = EventsRecorder(expectEvents = false), + roomMemberDetailsState = aUserProfileState(userId = A_USER_ID), + ), + onProfileClick = callback, + ) + rule.clickOn(R.string.screen_room_details_profile_row_title) + } + } } private fun AndroidComposeTestRule.setRoomDetailView( @@ -314,6 +332,7 @@ private fun AndroidComposeTestRule.setRoomD onPinnedMessagesClick: () -> Unit = EnsureNeverCalled(), onKnockRequestsClick: () -> Unit = EnsureNeverCalled(), onSecurityAndPrivacyClick: () -> Unit = EnsureNeverCalled(), + onProfileClick: (UserId) -> Unit = EnsureNeverCalledWithParam(), ) { setContent { RoomDetailsView( @@ -332,6 +351,7 @@ private fun AndroidComposeTestRule.setRoomD onPinnedMessagesClick = onPinnedMessagesClick, onKnockRequestsClick = onKnockRequestsClick, onSecurityAndPrivacyClick = onSecurityAndPrivacyClick, + onProfileClick = onProfileClick, ) } } diff --git a/features/userprofile/impl/build.gradle.kts b/features/userprofile/impl/build.gradle.kts index 2c94082d0c..70769c986d 100644 --- a/features/userprofile/impl/build.gradle.kts +++ b/features/userprofile/impl/build.gradle.kts @@ -33,6 +33,7 @@ dependencies { implementation(projects.libraries.androidutils) implementation(projects.libraries.mediaviewer.api) implementation(projects.features.call.api) + implementation(projects.features.verifysession.api) api(projects.features.userprofile.api) api(projects.features.userprofile.shared) implementation(libs.coil.compose) diff --git a/features/userprofile/impl/src/main/kotlin/io/element/android/features/userprofile/impl/UserProfileFlowNode.kt b/features/userprofile/impl/src/main/kotlin/io/element/android/features/userprofile/impl/UserProfileFlowNode.kt index 489d82480c..c09b582e6b 100644 --- a/features/userprofile/impl/src/main/kotlin/io/element/android/features/userprofile/impl/UserProfileFlowNode.kt +++ b/features/userprofile/impl/src/main/kotlin/io/element/android/features/userprofile/impl/UserProfileFlowNode.kt @@ -25,6 +25,7 @@ import io.element.android.features.call.api.ElementCallEntryPoint import io.element.android.features.userprofile.api.UserProfileEntryPoint import io.element.android.features.userprofile.impl.root.UserProfileNode import io.element.android.features.userprofile.shared.UserProfileNodeHelper +import io.element.android.features.verifysession.api.VerifySessionEntryPoint import io.element.android.libraries.architecture.BackstackView import io.element.android.libraries.architecture.BaseFlowNode import io.element.android.libraries.architecture.createNode @@ -32,7 +33,9 @@ import io.element.android.libraries.architecture.inputs import io.element.android.libraries.di.SessionScope import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.user.CurrentSessionIdHolder +import io.element.android.libraries.matrix.api.verification.VerificationRequest import io.element.android.libraries.mediaviewer.api.MediaViewerEntryPoint import kotlinx.parcelize.Parcelize @@ -43,6 +46,7 @@ class UserProfileFlowNode @AssistedInject constructor( private val elementCallEntryPoint: ElementCallEntryPoint, private val sessionIdHolder: CurrentSessionIdHolder, private val mediaViewerEntryPoint: MediaViewerEntryPoint, + private val verifySessionEntryPoint: VerifySessionEntryPoint, ) : BaseFlowNode( backstack = BackStack( initialElement = NavTarget.Root, @@ -57,6 +61,9 @@ class UserProfileFlowNode @AssistedInject constructor( @Parcelize data class AvatarPreview(val name: String, val avatarUrl: String) : NavTarget + + @Parcelize + data class VerifyUser(val userId: UserId) : NavTarget } override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { @@ -74,6 +81,10 @@ class UserProfileFlowNode @AssistedInject constructor( override fun onStartCall(dmRoomId: RoomId) { elementCallEntryPoint.startCall(CallType.RoomCall(sessionId = sessionIdHolder.current, roomId = dmRoomId)) } + + override fun onVerifyUser(userId: UserId) { + backstack.push(NavTarget.VerifyUser(userId)) + } } val params = UserProfileNode.UserProfileInputs(userId = inputs().userId) createNode(buildContext, listOf(callback, params)) @@ -96,6 +107,15 @@ class UserProfileFlowNode @AssistedInject constructor( .callback(callback) .build() } + is NavTarget.VerifyUser -> { + val params = VerifySessionEntryPoint.Params( + showDeviceVerifiedScreen = false, + verificationRequest = VerificationRequest.Outgoing.User(userId = navTarget.userId) + ) + verifySessionEntryPoint.nodeBuilder(this, buildContext) + .params(params) + .build() + } } } diff --git a/features/userprofile/impl/src/main/kotlin/io/element/android/features/userprofile/impl/root/UserProfileNode.kt b/features/userprofile/impl/src/main/kotlin/io/element/android/features/userprofile/impl/root/UserProfileNode.kt index 1b99e8409b..af2e08212c 100644 --- a/features/userprofile/impl/src/main/kotlin/io/element/android/features/userprofile/impl/root/UserProfileNode.kt +++ b/features/userprofile/impl/src/main/kotlin/io/element/android/features/userprofile/impl/root/UserProfileNode.kt @@ -75,6 +75,7 @@ class UserProfileNode @AssistedInject constructor( onOpenDm = ::onStartDM, onStartCall = callback::onStartCall, openAvatarPreview = callback::openAvatarPreview, + onVerifyClick = callback::onVerifyUser, ) } } diff --git a/features/userprofile/impl/src/main/kotlin/io/element/android/features/userprofile/impl/root/UserProfilePresenter.kt b/features/userprofile/impl/src/main/kotlin/io/element/android/features/userprofile/impl/root/UserProfilePresenter.kt index 8189f75aa3..a3378fee31 100644 --- a/features/userprofile/impl/src/main/kotlin/io/element/android/features/userprofile/impl/root/UserProfilePresenter.kt +++ b/features/userprofile/impl/src/main/kotlin/io/element/android/features/userprofile/impl/root/UserProfilePresenter.kt @@ -73,7 +73,6 @@ class UserProfilePresenter @AssistedInject constructor( val coroutineScope = rememberCoroutineScope() val isCurrentUser = remember { client.isMe(userId) } var confirmationDialog by remember { mutableStateOf(null) } - var userProfile by remember { mutableStateOf(null) } val startDmActionState: MutableState> = remember { mutableStateOf(AsyncAction.Uninitialized) } val isBlocked: MutableState> = remember { mutableStateOf(AsyncData.Uninitialized) } val isVerified: MutableState> = remember { mutableStateOf(AsyncData.Uninitialized) } @@ -86,9 +85,8 @@ class UserProfilePresenter @AssistedInject constructor( .onEach { isBlocked.value = AsyncData.Success(it) } .launchIn(this) } - LaunchedEffect(Unit) { - userProfile = client.getProfile(userId).getOrNull() - } + val userProfile by produceState(null) { value = client.getProfile(userId).getOrNull() } + LaunchedEffect(Unit) { suspend { client.encryptionService().isUserVerified(userId).getOrThrow() diff --git a/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileNodeHelper.kt b/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileNodeHelper.kt index deb72a5bab..61f4669769 100644 --- a/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileNodeHelper.kt +++ b/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileNodeHelper.kt @@ -24,6 +24,7 @@ class UserProfileNodeHelper( fun openAvatarPreview(username: String, avatarUrl: String) fun onStartDM(roomId: RoomId) fun onStartCall(dmRoomId: RoomId) + fun onVerifyUser(userId: UserId) } fun onShareUser( diff --git a/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileView.kt b/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileView.kt index dee44377d6..68ef207184 100644 --- a/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileView.kt +++ b/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileView.kt @@ -38,6 +38,7 @@ import io.element.android.libraries.designsystem.theme.components.Scaffold import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.TopAppBar import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.ui.components.CreateDmConfirmationBottomSheet import io.element.android.libraries.ui.strings.CommonStrings @@ -50,6 +51,7 @@ fun UserProfileView( onStartCall: (RoomId) -> Unit, goBack: () -> Unit, openAvatarPreview: (username: String, url: String) -> Unit, + onVerifyClick: (UserId) -> Unit, modifier: Modifier = Modifier, ) { Scaffold( @@ -82,7 +84,7 @@ fun UserProfileView( ) Spacer(modifier = Modifier.height(26.dp)) if (!state.isCurrentUser) { - VerifyUserSection(state) + VerifyUserSection(state, onVerifyClick = { onVerifyClick(state.userId) }) BlockUserSection(state) BlockUserDialogs(state) } @@ -116,14 +118,15 @@ fun UserProfileView( } @Composable -private fun VerifyUserSection(state: UserProfileState) { +private fun VerifyUserSection( + state: UserProfileState, + onVerifyClick: () -> Unit, +) { if (state.isVerified.dataOrNull() == false) { ListItem( - headlineContent = { Text(stringResource(CommonStrings.common_verify_identity)) }, - supportingContent = { Text(stringResource(R.string.screen_room_member_details_verify_button_subtitle)) }, + headlineContent = { Text(stringResource(CommonStrings.common_verify_user)) }, leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Lock())), - enabled = false, - onClick = { }, + onClick = onVerifyClick, ) } } @@ -139,6 +142,7 @@ internal fun UserProfileViewPreview( goBack = {}, onOpenDm = {}, onStartCall = {}, - openAvatarPreview = { _, _ -> } + openAvatarPreview = { _, _ -> }, + onVerifyClick = {}, ) } diff --git a/features/userprofile/shared/src/test/kotlin/io/element/android/features/userprofile/UserProfileViewTest.kt b/features/userprofile/shared/src/test/kotlin/io/element/android/features/userprofile/UserProfileViewTest.kt index 6c81acd00a..9a14a20221 100644 --- a/features/userprofile/shared/src/test/kotlin/io/element/android/features/userprofile/UserProfileViewTest.kt +++ b/features/userprofile/shared/src/test/kotlin/io/element/android/features/userprofile/UserProfileViewTest.kt @@ -20,8 +20,10 @@ import io.element.android.features.userprofile.shared.UserProfileView import io.element.android.features.userprofile.shared.aUserProfileState import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.test.AN_AVATAR_URL import io.element.android.libraries.matrix.test.A_ROOM_ID +import io.element.android.libraries.matrix.test.A_USER_ID import io.element.android.libraries.matrix.test.A_USER_NAME import io.element.android.libraries.testtags.TestTags import io.element.android.libraries.ui.strings.CommonStrings @@ -193,6 +195,17 @@ class UserProfileViewTest { rule.clickOn(CommonStrings.action_cancel) eventsRecorder.assertSingle(UserProfileEvents.ClearConfirmationDialog) } + + @Test + fun `on verify user clicked - the right callback is called`() = runTest { + ensureCalledOnceWithParam(A_USER_ID) { callback -> + rule.setUserProfileView( + state = aUserProfileState(userId = A_USER_ID, isVerified = AsyncData.Success(false)), + onVerifyClick = callback, + ) + rule.clickOn(CommonStrings.common_verify_user) + } + } } private fun AndroidComposeTestRule.setUserProfileView( @@ -202,6 +215,7 @@ private fun AndroidComposeTestRule.setUserP onShareUser: () -> Unit = EnsureNeverCalled(), onDmStarted: (RoomId) -> Unit = EnsureNeverCalledWithParam(), onStartCall: (RoomId) -> Unit = EnsureNeverCalledWithParam(), + onVerifyClick: (UserId) -> Unit = EnsureNeverCalledWithParam(), goBack: () -> Unit = EnsureNeverCalled(), openAvatarPreview: (String, String) -> Unit = EnsureNeverCalledWithTwoParams(), ) { @@ -213,6 +227,7 @@ private fun AndroidComposeTestRule.setUserP onStartCall = onStartCall, goBack = goBack, openAvatarPreview = openAvatarPreview, + onVerifyClick = onVerifyClick, ) } } diff --git a/features/verifysession/api/src/main/kotlin/io/element/android/features/verifysession/api/IncomingVerificationEntryPoint.kt b/features/verifysession/api/src/main/kotlin/io/element/android/features/verifysession/api/IncomingVerificationEntryPoint.kt index 2d2a789b3a..9d90f33e8d 100644 --- a/features/verifysession/api/src/main/kotlin/io/element/android/features/verifysession/api/IncomingVerificationEntryPoint.kt +++ b/features/verifysession/api/src/main/kotlin/io/element/android/features/verifysession/api/IncomingVerificationEntryPoint.kt @@ -12,11 +12,11 @@ import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.plugin.Plugin import io.element.android.libraries.architecture.FeatureEntryPoint import io.element.android.libraries.architecture.NodeInputs -import io.element.android.libraries.matrix.api.verification.SessionVerificationRequestDetails +import io.element.android.libraries.matrix.api.verification.VerificationRequest interface IncomingVerificationEntryPoint : FeatureEntryPoint { data class Params( - val sessionVerificationRequestDetails: SessionVerificationRequestDetails, + val verificationRequest: VerificationRequest.Incoming, ) : NodeInputs fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder diff --git a/features/verifysession/api/src/main/kotlin/io/element/android/features/verifysession/api/VerifySessionEntryPoint.kt b/features/verifysession/api/src/main/kotlin/io/element/android/features/verifysession/api/VerifySessionEntryPoint.kt index 3bae35a3cf..3dfc7a2962 100644 --- a/features/verifysession/api/src/main/kotlin/io/element/android/features/verifysession/api/VerifySessionEntryPoint.kt +++ b/features/verifysession/api/src/main/kotlin/io/element/android/features/verifysession/api/VerifySessionEntryPoint.kt @@ -12,9 +12,13 @@ import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.plugin.Plugin import io.element.android.libraries.architecture.FeatureEntryPoint import io.element.android.libraries.architecture.NodeInputs +import io.element.android.libraries.matrix.api.verification.VerificationRequest interface VerifySessionEntryPoint : FeatureEntryPoint { - data class Params(val showDeviceVerifiedScreen: Boolean) : NodeInputs + data class Params( + val showDeviceVerifiedScreen: Boolean, + val verificationRequest: VerificationRequest.Outgoing, + ) : NodeInputs fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder @@ -25,8 +29,8 @@ interface VerifySessionEntryPoint : FeatureEntryPoint { } interface Callback : Plugin { - fun onEnterRecoveryKey() - fun onResetKey() + fun onLearnMoreAboutEncryption() + fun onBack() fun onDone() } } diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationNode.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationNode.kt index f884f64510..802eadb13a 100644 --- a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationNode.kt +++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationNode.kt @@ -28,7 +28,7 @@ class IncomingVerificationNode @AssistedInject constructor( ) : Node(buildContext, plugins = plugins), IncomingVerificationNavigator { private val presenter = presenterFactory.create( - sessionVerificationRequestDetails = inputs().sessionVerificationRequestDetails, + verificationRequest = inputs().verificationRequest, navigator = this, ) 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 b3cec7def2..c241cef105 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 @@ -10,10 +10,12 @@ 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.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import com.freeletics.flowredux.compose.rememberStateAndDispatch import dagger.assisted.Assisted import dagger.assisted.AssistedFactory @@ -22,20 +24,25 @@ import io.element.android.features.verifysession.impl.incoming.IncomingVerificat 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.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 kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch import timber.log.Timber import io.element.android.features.verifysession.impl.incoming.IncomingVerificationStateMachine.Event as StateMachineEvent import io.element.android.features.verifysession.impl.incoming.IncomingVerificationStateMachine.State as StateMachineState class IncomingVerificationPresenter @AssistedInject constructor( - @Assisted private val sessionVerificationRequestDetails: SessionVerificationRequestDetails, + @Assisted private val verificationRequest: VerificationRequest.Incoming, @Assisted private val navigator: IncomingVerificationNavigator, + @SessionCoroutineScope private val sessionCoroutineScope: CoroutineScope, private val sessionVerificationService: SessionVerificationService, private val stateMachine: IncomingVerificationStateMachine, private val dateFormatter: DateFormatter, @@ -43,49 +50,63 @@ class IncomingVerificationPresenter @AssistedInject constructor( @AssistedFactory interface Factory { fun create( - sessionVerificationRequestDetails: SessionVerificationRequestDetails, + verificationRequest: VerificationRequest.Incoming, navigator: IncomingVerificationNavigator, ): IncomingVerificationPresenter } @Composable override fun present(): IncomingVerificationState { - LaunchedEffect(Unit) { - // Force reset, just in case the service was left in a broken state - sessionVerificationService.reset( - cancelAnyPendingVerificationAttempt = false - ) - // Acknowledge the request right now - sessionVerificationService.acknowledgeVerificationRequest(sessionVerificationRequestDetails) - } + val coroutineScope = rememberCoroutineScope { Dispatchers.IO } + val stateAndDispatch = stateMachine.rememberStateAndDispatch() + + DisposableEffect(Unit) { + coroutineScope.launch { + // Force reset, just in case the service was left in a broken state + sessionVerificationService.reset( + cancelAnyPendingVerificationAttempt = false + ) + + // Start this after observing state machine + observeVerificationService() + + // Acknowledge the request right now + sessionVerificationService.acknowledgeVerificationRequest(verificationRequest) + } + + onDispose { + sessionCoroutineScope.launch { + val currentState = stateAndDispatch.state.value + sessionVerificationService.reset( + cancelAnyPendingVerificationAttempt = currentState?.isPending() == true, + ) + } + } + } + val formattedSignInTime = remember { dateFormatter.format( - timestamp = sessionVerificationRequestDetails.firstSeenTimestamp, + timestamp = verificationRequest.details.firstSeenTimestamp, mode = DateFormatterMode.TimeOrDate, ) } val step by remember { derivedStateOf { stateAndDispatch.state.value.toVerificationStep( - sessionVerificationRequestDetails = sessionVerificationRequestDetails, + sessionVerificationRequestDetails = verificationRequest.details, formattedSignInTime = formattedSignInTime, ) } } LaunchedEffect(stateAndDispatch.state.value) { - if ((stateAndDispatch.state.value as? IncomingVerificationStateMachine.State.Initial)?.isCancelled == true) { + if ((stateAndDispatch.state.value as? StateMachineState.Initial)?.isCancelled == true) { // The verification was canceled before it was started, maybe because another session accepted it navigator.onFinish() } } - // Start this after observing state machine - LaunchedEffect(Unit) { - observeVerificationService() - } - fun handleEvents(event: IncomingVerificationViewEvents) { Timber.d("Verification user action: ${event::class.simpleName}") when (event) { @@ -119,6 +140,7 @@ class IncomingVerificationPresenter @AssistedInject constructor( return IncomingVerificationState( step = step, + request = verificationRequest, eventSink = ::handleEvents, ) } @@ -129,36 +151,36 @@ class IncomingVerificationPresenter @AssistedInject constructor( ): Step = when (val machineState = this) { is StateMachineState.Initial, - IncomingVerificationStateMachine.State.AcceptingIncomingVerification, - IncomingVerificationStateMachine.State.RejectingIncomingVerification, + StateMachineState.AcceptingIncomingVerification, + StateMachineState.RejectingIncomingVerification, null -> { Step.Initial( - deviceDisplayName = sessionVerificationRequestDetails.displayName ?: sessionVerificationRequestDetails.deviceId.value, + deviceDisplayName = sessionVerificationRequestDetails.senderProfile.displayName ?: sessionVerificationRequestDetails.deviceId.value, deviceId = sessionVerificationRequestDetails.deviceId, formattedSignInTime = formattedSignInTime, - isWaiting = machineState == IncomingVerificationStateMachine.State.AcceptingIncomingVerification || - machineState == IncomingVerificationStateMachine.State.RejectingIncomingVerification, + isWaiting = machineState == StateMachineState.AcceptingIncomingVerification || + machineState == StateMachineState.RejectingIncomingVerification, ) } - is IncomingVerificationStateMachine.State.ChallengeReceived -> + is StateMachineState.ChallengeReceived -> Step.Verifying( data = machineState.data, isWaiting = false, ) - IncomingVerificationStateMachine.State.Completed -> Step.Completed - IncomingVerificationStateMachine.State.Canceling, - IncomingVerificationStateMachine.State.Failure -> Step.Failure - is IncomingVerificationStateMachine.State.AcceptingChallenge -> + StateMachineState.Completed -> Step.Completed + StateMachineState.Canceling, + StateMachineState.Failure -> Step.Failure + is StateMachineState.AcceptingChallenge -> Step.Verifying( data = machineState.data, isWaiting = true, ) - is IncomingVerificationStateMachine.State.RejectingChallenge -> + is StateMachineState.RejectingChallenge -> Step.Verifying( data = machineState.data, isWaiting = true, ) - IncomingVerificationStateMachine.State.Canceled -> Step.Canceled + StateMachineState.Canceled -> Step.Canceled } private fun CoroutineScope.observeVerificationService() { @@ -170,10 +192,10 @@ class IncomingVerificationPresenter @AssistedInject constructor( VerificationFlowState.DidAcceptVerificationRequest, VerificationFlowState.DidStartSasVerification -> Unit is VerificationFlowState.DidReceiveVerificationData -> { - stateMachine.dispatch(IncomingVerificationStateMachine.Event.DidReceiveChallenge(verificationAttemptState.data)) + stateMachine.dispatch(StateMachineEvent.DidReceiveChallenge(verificationAttemptState.data)) } VerificationFlowState.DidFinish -> { - stateMachine.dispatch(IncomingVerificationStateMachine.Event.DidAcceptChallenge) + stateMachine.dispatch(StateMachineEvent.DidAcceptChallenge) } VerificationFlowState.DidCancel -> { // Can happen when: @@ -181,10 +203,10 @@ class IncomingVerificationPresenter @AssistedInject constructor( // - another session has accepted the incoming verification request // - the user reject the challenge from this application (I think this is an error). In this case, the state // machine will ignore this event and change state to Failure. - stateMachine.dispatch(IncomingVerificationStateMachine.Event.DidCancel) + stateMachine.dispatch(StateMachineEvent.DidCancel) } VerificationFlowState.DidFail -> { - stateMachine.dispatch(IncomingVerificationStateMachine.Event.DidFail) + stateMachine.dispatch(StateMachineEvent.DidFail) } } } 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 241ea8e5ad..95f81af479 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 @@ -11,10 +11,12 @@ import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable import io.element.android.libraries.matrix.api.core.DeviceId import io.element.android.libraries.matrix.api.verification.SessionVerificationData +import io.element.android.libraries.matrix.api.verification.VerificationRequest @Immutable data class IncomingVerificationState( val step: Step, + val request: VerificationRequest.Incoming, val eventSink: (IncomingVerificationViewEvents) -> Unit, ) { @Stable diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationStateMachine.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationStateMachine.kt index d873c6fd50..9ff9f50cd8 100644 --- a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationStateMachine.kt +++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationStateMachine.kt @@ -26,7 +26,7 @@ class IncomingVerificationStateMachine @Inject constructor( init { spec { inState { - on { _: Event.AcceptIncomingRequest, state -> + on { _, state -> state.override { State.AcceptingIncomingVerification.andLogStateChange() } } } @@ -39,23 +39,23 @@ class IncomingVerificationStateMachine @Inject constructor( } } inState { - on { _: Event.AcceptChallenge, state -> + on { _, state -> state.override { State.AcceptingChallenge(state.snapshot.data).andLogStateChange() } } - on { _: Event.DeclineChallenge, state -> + on { _, state -> state.override { State.RejectingChallenge(state.snapshot.data).andLogStateChange() } } } inState { - onEnterEffect { _ -> + onEnterEffect { sessionVerificationService.approveVerification() } - on { _: Event.DidAcceptChallenge, state -> + on { _, state -> state.override { State.Completed.andLogStateChange() } } } inState { - onEnterEffect { _ -> + onEnterEffect { sessionVerificationService.declineVerification() } } @@ -66,7 +66,7 @@ class IncomingVerificationStateMachine @Inject constructor( } inState { logReceivedEvents() - on { _: Event.Cancel, state: MachineState -> + on { _, state: MachineState -> when (state.snapshot) { State.Completed, State.Canceled -> state.noChange() else -> { @@ -75,7 +75,7 @@ class IncomingVerificationStateMachine @Inject constructor( } } } - on { _: Event.DidCancel, state: MachineState -> + on { _, state: MachineState -> when (state.snapshot) { is State.RejectingChallenge -> { state.override { State.Failure.andLogStateChange() } @@ -91,7 +91,7 @@ class IncomingVerificationStateMachine @Inject constructor( State.Failure -> state.noChange() } } - on { _: Event.DidFail, state: MachineState -> + on { _, state: MachineState -> state.override { State.Failure.andLogStateChange() } } } @@ -128,6 +128,11 @@ class IncomingVerificationStateMachine @Inject constructor( /** Verification failure. */ data object Failure : State + + fun isPending(): Boolean = when (this) { + AcceptingIncomingVerification, RejectingIncomingVerification, Failure, is ChallengeReceived, is AcceptingChallenge, is RejectingChallenge -> true + is Initial, Canceling, Canceled, Completed -> false + } } sealed interface Event { diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationStateProvider.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationStateProvider.kt index d57f56385b..cdad2669d6 100644 --- a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationStateProvider.kt +++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationStateProvider.kt @@ -12,16 +12,32 @@ 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.matrix.api.core.DeviceId +import io.element.android.libraries.matrix.api.core.FlowId +import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.verification.SessionVerificationRequestDetails +import io.element.android.libraries.matrix.api.verification.VerificationRequest open class IncomingVerificationStateProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( anIncomingVerificationState(), - anIncomingVerificationState(step = aStepInitial(isWaiting = true)), + anIncomingVerificationState(step = aStepInitial(isWaiting = false), verificationRequest = anIncomingSessionVerificationRequest()), + anIncomingVerificationState(step = aStepInitial(isWaiting = false), verificationRequest = anIncomingUserVerificationRequest()), + anIncomingVerificationState(step = aStepInitial(isWaiting = true), verificationRequest = anIncomingSessionVerificationRequest()), + anIncomingVerificationState(step = aStepInitial(isWaiting = true), verificationRequest = anIncomingUserVerificationRequest()), anIncomingVerificationState(step = Step.Verifying(data = aEmojisSessionVerificationData(), isWaiting = false)), + anIncomingVerificationState( + step = Step.Verifying(data = aEmojisSessionVerificationData(), isWaiting = false), + verificationRequest = anIncomingUserVerificationRequest() + ), anIncomingVerificationState(step = Step.Verifying(data = aEmojisSessionVerificationData(), isWaiting = true)), + anIncomingVerificationState( + step = Step.Verifying(data = aEmojisSessionVerificationData(), isWaiting = true), + verificationRequest = anIncomingUserVerificationRequest() + ), anIncomingVerificationState(step = Step.Verifying(data = aDecimalsSessionVerificationData(), isWaiting = false)), anIncomingVerificationState(step = Step.Completed), + anIncomingVerificationState(step = Step.Completed, verificationRequest = anIncomingUserVerificationRequest()), anIncomingVerificationState(step = Step.Failure), anIncomingVerificationState(step = Step.Canceled), // Add other state here @@ -37,10 +53,38 @@ internal fun aStepInitial( isWaiting = isWaiting, ) +internal fun anIncomingSessionVerificationRequest() = VerificationRequest.Incoming.OtherSession( + details = SessionVerificationRequestDetails( + senderProfile = SessionVerificationRequestDetails.SenderProfile( + userId = UserId("@alice:example.com"), + displayName = "Alice", + avatarUrl = null, + ), + flowId = FlowId("1234"), + deviceId = DeviceId("ILAKNDNASDLK"), + firstSeenTimestamp = 0, + ) +) + +internal fun anIncomingUserVerificationRequest() = VerificationRequest.Incoming.User( + details = SessionVerificationRequestDetails( + senderProfile = SessionVerificationRequestDetails.SenderProfile( + userId = UserId("@alice:example.com"), + displayName = "Alice", + avatarUrl = null, + ), + flowId = FlowId("1234"), + deviceId = DeviceId("ILAKNDNASDLK"), + firstSeenTimestamp = 0, + ) +) + internal fun anIncomingVerificationState( step: Step = aStepInitial(), + verificationRequest: VerificationRequest.Incoming = anIncomingSessionVerificationRequest(), eventSink: (IncomingVerificationViewEvents) -> Unit = {}, ) = IncomingVerificationState( step = step, + request = verificationRequest, eventSink = eventSink, ) diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationView.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationView.kt index b31c937f36..0bafdbbb24 100644 --- a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationView.kt +++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationView.kt @@ -27,17 +27,19 @@ import io.element.android.features.verifysession.impl.incoming.IncomingVerificat import io.element.android.features.verifysession.impl.incoming.ui.SessionDetailsView import io.element.android.features.verifysession.impl.ui.VerificationBottomMenu import io.element.android.features.verifysession.impl.ui.VerificationContentVerifying +import io.element.android.features.verifysession.impl.ui.VerificationUserProfileContent import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage import io.element.android.libraries.designsystem.components.BigIcon import io.element.android.libraries.designsystem.components.PageTitle +import io.element.android.libraries.designsystem.components.button.BackButton 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.InvisibleButton import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.TextButton import io.element.android.libraries.designsystem.theme.components.TopAppBar import io.element.android.libraries.matrix.api.verification.SessionVerificationData +import io.element.android.libraries.matrix.api.verification.VerificationRequest import io.element.android.libraries.ui.strings.CommonStrings /** @@ -59,10 +61,17 @@ fun IncomingVerificationView( topBar = { TopAppBar( title = {}, + navigationIcon = { + when { + step is Step.Initial && !step.isWaiting -> Unit + step is Step.Completed -> Unit + else -> BackButton(onClick = { state.eventSink(IncomingVerificationViewEvents.GoBack) }) + } + } ) }, header = { - IncomingVerificationHeader(step = step) + IncomingVerificationHeader(step = step, request = state.request) }, footer = { IncomingVerificationBottomMenu( @@ -72,37 +81,58 @@ fun IncomingVerificationView( ) { IncomingVerificationContent( step = step, + request = state.request, ) } } @Composable -private fun IncomingVerificationHeader(step: Step) { +private fun IncomingVerificationHeader(step: Step, request: VerificationRequest.Incoming) { val iconStyle = when (step) { Step.Canceled -> BigIcon.Style.AlertSolid - is Step.Initial -> BigIcon.Style.Default(CompoundIcons.LockSolid()) - is Step.Verifying -> BigIcon.Style.Default(CompoundIcons.Reaction()) + is Step.Initial -> if (step.isWaiting) { + BigIcon.Style.Loading + } else { + when (request) { + is VerificationRequest.Incoming.OtherSession -> BigIcon.Style.Default(CompoundIcons.LockSolid()) + is VerificationRequest.Incoming.User -> BigIcon.Style.Default(CompoundIcons.UserProfileSolid()) + } + } + is Step.Verifying -> if (step.isWaiting) { + BigIcon.Style.Loading + } else { + BigIcon.Style.Default(CompoundIcons.ReactionSolid()) + } Step.Completed -> BigIcon.Style.SuccessSolid Step.Failure -> BigIcon.Style.AlertSolid } val titleTextId = when (step) { - Step.Canceled -> R.string.screen_session_verification_request_failure_title + Step.Canceled -> CommonStrings.common_verification_failed is Step.Initial -> R.string.screen_session_verification_request_title is Step.Verifying -> when (step.data) { is SessionVerificationData.Decimals -> R.string.screen_session_verification_compare_numbers_title is SessionVerificationData.Emojis -> R.string.screen_session_verification_compare_emojis_title } - Step.Completed -> R.string.screen_session_verification_request_success_title + Step.Completed -> CommonStrings.common_verification_complete Step.Failure -> R.string.screen_session_verification_request_failure_title } val subtitleTextId = when (step) { Step.Canceled -> R.string.screen_session_verification_request_failure_subtitle - is Step.Initial -> R.string.screen_session_verification_request_subtitle + is Step.Initial -> when (request) { + is VerificationRequest.Incoming.OtherSession -> R.string.screen_session_verification_request_subtitle + is VerificationRequest.Incoming.User -> R.string.screen_session_verification_user_responder_subtitle + } is Step.Verifying -> when (step.data) { is SessionVerificationData.Decimals -> R.string.screen_session_verification_compare_numbers_subtitle - is SessionVerificationData.Emojis -> R.string.screen_session_verification_compare_emojis_subtitle + is SessionVerificationData.Emojis -> when (request) { + is VerificationRequest.Incoming.OtherSession -> R.string.screen_session_verification_compare_emojis_subtitle + is VerificationRequest.Incoming.User -> R.string.screen_session_verification_compare_emojis_user_subtitle + } + } + Step.Completed -> when (request) { + is VerificationRequest.Incoming.OtherSession -> R.string.screen_session_verification_complete_subtitle + is VerificationRequest.Incoming.User -> R.string.screen_session_verification_complete_user_subtitle } - Step.Completed -> R.string.screen_session_verification_request_success_subtitle Step.Failure -> R.string.screen_session_verification_request_failure_subtitle } PageTitle( @@ -115,9 +145,10 @@ private fun IncomingVerificationHeader(step: Step) { @Composable private fun IncomingVerificationContent( step: Step, + request: VerificationRequest.Incoming, ) { when (step) { - is Step.Initial -> ContentInitial(step) + is Step.Initial -> ContentInitial(step, request) is Step.Verifying -> VerificationContentVerifying(step.data) else -> Unit } @@ -126,24 +157,40 @@ private fun IncomingVerificationContent( @Composable private fun ContentInitial( initialIncoming: Step.Initial, + request: VerificationRequest.Incoming, ) { - Column( - modifier = Modifier.fillMaxWidth(), - verticalArrangement = Arrangement.spacedBy(24.dp), - ) { - SessionDetailsView( - deviceName = initialIncoming.deviceDisplayName, - deviceId = initialIncoming.deviceId, - signInFormattedTimestamp = initialIncoming.formattedSignInTime, - ) - Text( - modifier = Modifier - .align(Alignment.CenterHorizontally) - .padding(bottom = 16.dp), - text = stringResource(R.string.screen_session_verification_request_footer), - style = ElementTheme.typography.fontBodyMdMedium, - textAlign = TextAlign.Center, - ) + when (request) { + is VerificationRequest.Incoming.OtherSession -> { + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(24.dp), + ) { + SessionDetailsView( + deviceName = initialIncoming.deviceDisplayName, + deviceId = initialIncoming.deviceId, + signInFormattedTimestamp = initialIncoming.formattedSignInTime, + ) + Text( + modifier = Modifier + .align(Alignment.CenterHorizontally) + .padding(bottom = 16.dp), + text = stringResource(R.string.screen_session_verification_request_footer), + style = ElementTheme.typography.fontBodyMdMedium, + textAlign = TextAlign.Center, + ) + } + } + is VerificationRequest.Incoming.User -> { + Column( + modifier = Modifier.fillMaxWidth().padding(top = 24.dp), + ) { + VerificationUserProfileContent( + userId = request.details.senderProfile.userId, + displayName = request.details.senderProfile.displayName, + avatarUrl = request.details.senderProfile.avatarUrl, + ) + } + } } } @@ -157,16 +204,7 @@ private fun IncomingVerificationBottomMenu( when (step) { is Step.Initial -> { if (step.isWaiting) { - VerificationBottomMenu { - Button( - modifier = Modifier.fillMaxWidth(), - text = stringResource(R.string.screen_identity_waiting_on_other_device), - onClick = {}, - enabled = false, - showProgress = true, - ) - InvisibleButton() - } + // Show nothing } else { VerificationBottomMenu { Button( @@ -184,16 +222,7 @@ private fun IncomingVerificationBottomMenu( } is Step.Verifying -> { if (step.isWaiting) { - VerificationBottomMenu { - Button( - modifier = Modifier.fillMaxWidth(), - text = stringResource(R.string.screen_session_verification_positive_button_verifying_ongoing), - onClick = {}, - enabled = false, - showProgress = true, - ) - InvisibleButton() - } + // Show nothing } else { VerificationBottomMenu { Button( diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/VerifySelfSessionNode.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/VerifySelfSessionNode.kt index dbd39c4a12..23236b32e0 100644 --- a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/VerifySelfSessionNode.kt +++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/VerifySelfSessionNode.kt @@ -7,8 +7,6 @@ package io.element.android.features.verifysession.impl.outgoing -import android.app.Activity -import androidx.activity.compose.LocalActivity import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import com.bumble.appyx.core.modality.BuildContext @@ -18,10 +16,7 @@ import com.bumble.appyx.core.plugin.plugins import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode -import io.element.android.appconfig.LearnMoreConfig -import io.element.android.compound.theme.ElementTheme import io.element.android.features.verifysession.api.VerifySessionEntryPoint -import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab import io.element.android.libraries.architecture.inputs import io.element.android.libraries.di.SessionScope @@ -33,28 +28,22 @@ class VerifySelfSessionNode @AssistedInject constructor( ) : Node(buildContext, plugins = plugins) { private val callback = plugins().first() - private val presenter = presenterFactory.create( - showDeviceVerifiedScreen = inputs().showDeviceVerifiedScreen, - ) + private val inputs = inputs() - private fun onLearnMoreClick(activity: Activity, dark: Boolean) { - activity.openUrlInChromeCustomTab(null, dark, LearnMoreConfig.ENCRYPTION_URL) - } + private val presenter = presenterFactory.create( + showDeviceVerifiedScreen = inputs.showDeviceVerifiedScreen, + verificationRequest = inputs.verificationRequest, + ) @Composable override fun View(modifier: Modifier) { val state = presenter.present() - val activity = requireNotNull(LocalActivity.current) - val isDark = ElementTheme.isLightTheme.not() VerifySelfSessionView( state = state, modifier = modifier, - onLearnMoreClick = { - onLearnMoreClick(activity, isDark) - }, - onEnterRecoveryKey = callback::onEnterRecoveryKey, - onResetKey = callback::onResetKey, + onLearnMoreClick = callback::onLearnMoreAboutEncryption, onFinish = callback::onDone, + onBack = callback::onBack, ) } } diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/VerifySelfSessionPresenter.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/VerifySelfSessionPresenter.kt index c0760074ed..e7a05aa3e1 100644 --- a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/VerifySelfSessionPresenter.kt +++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/VerifySelfSessionPresenter.kt @@ -11,137 +11,113 @@ package io.element.android.features.verifysession.impl.outgoing import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.MutableState import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import com.freeletics.flowredux.compose.rememberStateAndDispatch import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject -import io.element.android.features.logout.api.LogoutUseCase -import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.Presenter -import io.element.android.libraries.architecture.runCatchingUpdatingState -import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.matrix.api.encryption.EncryptionService -import io.element.android.libraries.matrix.api.encryption.RecoveryState import io.element.android.libraries.matrix.api.verification.SessionVerificationService import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus import io.element.android.libraries.matrix.api.verification.VerificationFlowState -import io.element.android.libraries.preferences.api.store.SessionPreferencesStore +import io.element.android.libraries.matrix.api.verification.VerificationRequest import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.launch import timber.log.Timber import io.element.android.features.verifysession.impl.outgoing.VerifySelfSessionStateMachine.Event as StateMachineEvent import io.element.android.features.verifysession.impl.outgoing.VerifySelfSessionStateMachine.State as StateMachineState class VerifySelfSessionPresenter @AssistedInject constructor( @Assisted private val showDeviceVerifiedScreen: Boolean, + @Assisted private val verificationRequest: VerificationRequest.Outgoing, private val sessionVerificationService: SessionVerificationService, private val encryptionService: EncryptionService, - private val stateMachine: VerifySelfSessionStateMachine, - private val buildMeta: BuildMeta, - private val sessionPreferencesStore: SessionPreferencesStore, - private val logoutUseCase: LogoutUseCase, ) : Presenter { @AssistedFactory interface Factory { - fun create(showDeviceVerifiedScreen: Boolean): VerifySelfSessionPresenter + fun create( + verificationRequest: VerificationRequest.Outgoing, + showDeviceVerifiedScreen: Boolean, + ): VerifySelfSessionPresenter } + private val stateMachine = VerifySelfSessionStateMachine( + sessionVerificationService = sessionVerificationService, + encryptionService = encryptionService, + ) + @Composable override fun present(): VerifySelfSessionState { - val coroutineScope = rememberCoroutineScope() - LaunchedEffect(Unit) { - // Force reset, just in case the service was left in a broken state - sessionVerificationService.reset(true) - } - val recoveryState by encryptionService.recoveryStateStateFlow.collectAsState() val stateAndDispatch = stateMachine.rememberStateAndDispatch() - val skipVerification by sessionPreferencesStore.isSessionVerificationSkipped().collectAsState(initial = false) + val sessionVerifiedStatus by sessionVerificationService.sessionVerifiedStatus.collectAsState() - val signOutAction = remember { - mutableStateOf>(AsyncAction.Uninitialized) - } val step by remember { derivedStateOf { - if (skipVerification) { - VerifySelfSessionState.Step.Skipped - } else { - when (sessionVerifiedStatus) { - SessionVerifiedStatus.Unknown -> VerifySelfSessionState.Step.Loading - SessionVerifiedStatus.NotVerified -> { - stateAndDispatch.state.value.toVerificationStep( - canEnterRecoveryKey = recoveryState == RecoveryState.INCOMPLETE - ) - } - SessionVerifiedStatus.Verified -> { - if (stateAndDispatch.state.value != StateMachineState.Initial || showDeviceVerifiedScreen) { - // The user has verified the session, we need to show the success screen - VerifySelfSessionState.Step.Completed - } else { - // Automatic verification, which can happen on freshly created account, in this case, skip the screen - VerifySelfSessionState.Step.Skipped + when (verificationRequest) { + is VerificationRequest.Outgoing.CurrentSession -> { + when (sessionVerifiedStatus) { + SessionVerifiedStatus.Unknown -> VerifySelfSessionState.Step.Loading + SessionVerifiedStatus.NotVerified -> { + stateAndDispatch.state.value.toVerificationStep() + } + SessionVerifiedStatus.Verified -> { + if (stateAndDispatch.state.value != StateMachineState.Initial || showDeviceVerifiedScreen) { + // The user has verified the session, we need to show the success screen + VerifySelfSessionState.Step.Completed + } else { + // Automatic verification, which can happen on freshly created account, in this case, skip the screen + VerifySelfSessionState.Step.Exit + } } } } + is VerificationRequest.Outgoing.User -> stateAndDispatch.state.value.toVerificationStep() } } } + // Start this after observing state machine LaunchedEffect(Unit) { + // Force reset, just in case the service was left in a broken state + sessionVerificationService.reset(cancelAnyPendingVerificationAttempt = true) + observeVerificationService() } fun handleEvents(event: VerifySelfSessionViewEvents) { Timber.d("Verification user action: ${event::class.simpleName}") when (event) { - VerifySelfSessionViewEvents.UseAnotherDevice -> stateAndDispatch.dispatchAction(StateMachineEvent.UseAnotherDevice) - VerifySelfSessionViewEvents.RequestVerification -> stateAndDispatch.dispatchAction(StateMachineEvent.RequestVerification) + // Just relay the event to the state machine + VerifySelfSessionViewEvents.RequestVerification -> stateAndDispatch.dispatchAction(StateMachineEvent.RequestVerification(verificationRequest)) VerifySelfSessionViewEvents.StartSasVerification -> stateAndDispatch.dispatchAction(StateMachineEvent.StartSasVerification) VerifySelfSessionViewEvents.ConfirmVerification -> stateAndDispatch.dispatchAction(StateMachineEvent.AcceptChallenge) VerifySelfSessionViewEvents.DeclineVerification -> stateAndDispatch.dispatchAction(StateMachineEvent.DeclineChallenge) VerifySelfSessionViewEvents.Cancel -> stateAndDispatch.dispatchAction(StateMachineEvent.Cancel) VerifySelfSessionViewEvents.Reset -> stateAndDispatch.dispatchAction(StateMachineEvent.Reset) - VerifySelfSessionViewEvents.SignOut -> coroutineScope.signOut(signOutAction) - VerifySelfSessionViewEvents.SkipVerification -> coroutineScope.launch { - sessionPreferencesStore.setSkipSessionVerification(true) - } } } return VerifySelfSessionState( step = step, - signOutAction = signOutAction.value, - displaySkipButton = buildMeta.isDebuggable, + request = verificationRequest, eventSink = ::handleEvents, ) } - private fun StateMachineState?.toVerificationStep( - canEnterRecoveryKey: Boolean - ): VerifySelfSessionState.Step = + private fun StateMachineState?.toVerificationStep(): VerifySelfSessionState.Step = when (val machineState = this) { StateMachineState.Initial, null -> { - VerifySelfSessionState.Step.Initial( - canEnterRecoveryKey = canEnterRecoveryKey, - isLastDevice = encryptionService.isLastDevice.value - ) + VerifySelfSessionState.Step.Initial } - VerifySelfSessionStateMachine.State.UseAnotherDevice -> { - VerifySelfSessionState.Step.UseAnotherDevice - } - StateMachineState.RequestingVerification, - StateMachineState.StartingSasVerification, - StateMachineState.SasVerificationStarted, - StateMachineState.Canceling -> { + is StateMachineState.RequestingVerification, + is StateMachineState.StartingSasVerification, + StateMachineState.SasVerificationStarted -> { VerifySelfSessionState.Step.AwaitingOtherDeviceResponse } @@ -149,7 +125,7 @@ class VerifySelfSessionPresenter @AssistedInject constructor( VerifySelfSessionState.Step.Ready } - StateMachineState.Canceled -> { + is StateMachineState.Canceled -> { VerifySelfSessionState.Step.Canceled } @@ -164,6 +140,10 @@ class VerifySelfSessionPresenter @AssistedInject constructor( StateMachineState.Completed -> { VerifySelfSessionState.Step.Completed } + + StateMachineState.Exit -> { + VerifySelfSessionState.Step.Exit + } } private fun CoroutineScope.observeVerificationService() { @@ -171,33 +151,27 @@ class VerifySelfSessionPresenter @AssistedInject constructor( .onEach { Timber.d("Verification flow state: ${it::class.simpleName}") } .onEach { verificationAttemptState -> when (verificationAttemptState) { - VerificationFlowState.Initial -> stateMachine.dispatch(VerifySelfSessionStateMachine.Event.Reset) + VerificationFlowState.Initial -> stateMachine.dispatch(StateMachineEvent.Reset) VerificationFlowState.DidAcceptVerificationRequest -> { - stateMachine.dispatch(VerifySelfSessionStateMachine.Event.DidAcceptVerificationRequest) + stateMachine.dispatch(StateMachineEvent.DidAcceptVerificationRequest) } VerificationFlowState.DidStartSasVerification -> { - stateMachine.dispatch(VerifySelfSessionStateMachine.Event.DidStartSasVerification) + stateMachine.dispatch(StateMachineEvent.DidStartSasVerification) } is VerificationFlowState.DidReceiveVerificationData -> { - stateMachine.dispatch(VerifySelfSessionStateMachine.Event.DidReceiveChallenge(verificationAttemptState.data)) + stateMachine.dispatch(StateMachineEvent.DidReceiveChallenge(verificationAttemptState.data)) } VerificationFlowState.DidFinish -> { - stateMachine.dispatch(VerifySelfSessionStateMachine.Event.DidAcceptChallenge) + stateMachine.dispatch(StateMachineEvent.DidAcceptChallenge) } VerificationFlowState.DidCancel -> { - stateMachine.dispatch(VerifySelfSessionStateMachine.Event.DidCancel) + stateMachine.dispatch(StateMachineEvent.DidCancel) } VerificationFlowState.DidFail -> { - stateMachine.dispatch(VerifySelfSessionStateMachine.Event.DidFail) + stateMachine.dispatch(StateMachineEvent.DidFail) } } } .launchIn(this) } - - private fun CoroutineScope.signOut(signOutAction: MutableState>) = launch { - suspend { - logoutUseCase.logout(ignoreSdkError = true) - }.runCatchingUpdatingState(signOutAction) - } } diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/VerifySelfSessionState.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/VerifySelfSessionState.kt index 3c998247c7..a8f5a6771b 100644 --- a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/VerifySelfSessionState.kt +++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/VerifySelfSessionState.kt @@ -9,29 +9,25 @@ package io.element.android.features.verifysession.impl.outgoing import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable -import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.matrix.api.verification.SessionVerificationData +import io.element.android.libraries.matrix.api.verification.VerificationRequest @Immutable data class VerifySelfSessionState( val step: Step, - val signOutAction: AsyncAction, - val displaySkipButton: Boolean, + val request: VerificationRequest.Outgoing, val eventSink: (VerifySelfSessionViewEvents) -> Unit, ) { @Stable sealed interface Step { data object Loading : Step - - // FIXME canEnterRecoveryKey value is never read. - data class Initial(val canEnterRecoveryKey: Boolean, val isLastDevice: Boolean = false) : Step - data object UseAnotherDevice : Step + data object Initial : Step data object Canceled : Step data object AwaitingOtherDeviceResponse : Step data object Ready : Step data class Verifying(val data: SessionVerificationData, val state: AsyncData) : Step data object Completed : Step - data object Skipped : Step + data object Exit : Step } } diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/VerifySelfSessionStateMachine.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/VerifySelfSessionStateMachine.kt index a71b09c783..746cbe8aa4 100644 --- a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/VerifySelfSessionStateMachine.kt +++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/VerifySelfSessionStateMachine.kt @@ -19,39 +19,37 @@ import io.element.android.libraries.matrix.api.encryption.EncryptionService import io.element.android.libraries.matrix.api.encryption.RecoveryState import io.element.android.libraries.matrix.api.verification.SessionVerificationData import io.element.android.libraries.matrix.api.verification.SessionVerificationService +import io.element.android.libraries.matrix.api.verification.VerificationRequest import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.timeout -import javax.inject.Inject import kotlin.time.Duration.Companion.seconds import com.freeletics.flowredux.dsl.State as MachineState @OptIn(FlowPreview::class) -class VerifySelfSessionStateMachine @Inject constructor( +class VerifySelfSessionStateMachine( private val sessionVerificationService: SessionVerificationService, private val encryptionService: EncryptionService, ) : FlowReduxStateMachine( - initialState = State.Initial + initialState = State.Initial, ) { init { spec { inState { - on { _: Event.UseAnotherDevice, state -> - state.override { State.UseAnotherDevice.andLogStateChange() } - } - } - inState { - on { _: Event.RequestVerification, state -> - state.override { State.RequestingVerification.andLogStateChange() } + on { event, state -> + state.override { State.RequestingVerification(event.verificationRequest).andLogStateChange() } } } inState { - onEnterEffect { - sessionVerificationService.requestVerification() + onEnterEffect { event -> + when (event.verificationRequest) { + is VerificationRequest.Outgoing.CurrentSession -> sessionVerificationService.requestCurrentSessionVerification() + is VerificationRequest.Outgoing.User -> sessionVerificationService.requestUserVerification(event.verificationRequest.userId) + } } - on { _: Event.DidAcceptVerificationRequest, state -> + on { _, state -> state.override { State.VerificationRequestAccepted.andLogStateChange() } } } @@ -61,25 +59,26 @@ class VerifySelfSessionStateMachine @Inject constructor( } } inState { - on { _: Event.StartSasVerification, state -> + on { _, state -> state.override { State.StartingSasVerification.andLogStateChange() } } } inState { - on { _: Event.Reset, state -> + on { _, state -> + sessionVerificationService.reset(cancelAnyPendingVerificationAttempt = false) state.override { State.Initial.andLogStateChange() } } } inState { - on { event: Event.DidReceiveChallenge, state -> + on { event, state -> state.override { State.Verifying.ChallengeReceived(event.data).andLogStateChange() } } } inState { - on { _: Event.AcceptChallenge, state -> + on { _, state -> state.override { State.Verifying.Replying(state.snapshot.data, accept = true).andLogStateChange() } } - on { _: Event.DeclineChallenge, state -> + on { _, state -> state.override { State.Verifying.Replying(state.snapshot.data, accept = false).andLogStateChange() } } } @@ -91,7 +90,7 @@ class VerifySelfSessionStateMachine @Inject constructor( sessionVerificationService.declineVerification() } } - on { _: Event.DidAcceptChallenge, state -> + on { _, state -> // If a key backup exists, wait until it's restored or a timeout happens val hasBackup = encryptionService.doesBackupExistOnServer().getOrNull().orFalse() if (hasBackup) { @@ -104,21 +103,14 @@ class VerifySelfSessionStateMachine @Inject constructor( state.override { State.Completed.andLogStateChange() } } } - inState { - // TODO The 'Canceling' -> 'Canceled' transitions doesn't seem to work anymore, check if something changed in the Rust SDK - onEnterEffect { - sessionVerificationService.cancelVerification() - } - } inState { logReceivedEvents() - on { _: Event.DidStartSasVerification, state: MachineState -> + on { _, state: MachineState -> state.override { State.SasVerificationStarted.andLogStateChange() } } - on { _: Event.Cancel, state: MachineState -> + on { event, state: MachineState -> when (state.snapshot) { - State.Initial, State.Completed, State.Canceled -> state.noChange() - State.UseAnotherDevice -> state.override { State.Initial.andLogStateChange() } + State.Initial, State.Completed, is State.Canceled -> state.override { State.Exit } // For some reason `cancelVerification` is not calling its delegate `didCancel` method so we don't pass from // `Canceling` state to `Canceled` automatically anymore else -> { @@ -127,28 +119,22 @@ class VerifySelfSessionStateMachine @Inject constructor( } } } - on { _: Event.DidCancel, state: MachineState -> + on { event, state: MachineState -> state.override { State.Canceled.andLogStateChange() } } - on { _: Event.DidFail, state: MachineState -> - when (state.snapshot) { - is State.RequestingVerification -> state.override { State.Initial.andLogStateChange() } - else -> state.override { State.Canceled.andLogStateChange() } - } + on { event, state: MachineState -> + state.override { State.Canceled.andLogStateChange() } } } } } sealed interface State { - /** The initial state, before verification started. */ + /** Let the user know that they need to get ready on their other session. */ data object Initial : State - /** Let the user know that they need to get ready on their other session. */ - data object UseAnotherDevice : State - /** Waiting for verification acceptance. */ - data object RequestingVerification : State + data class RequestingVerification(val verificationRequest: VerificationRequest.Outgoing) : State /** Verification request accepted. Waiting for start. */ data object VerificationRequestAccepted : State @@ -167,22 +153,18 @@ class VerifySelfSessionStateMachine @Inject constructor( data class Replying(override val data: SessionVerificationData, val accept: Boolean) : Verifying(data) } - /** The verification is being canceled. */ - data object Canceling : State - /** The verification has been canceled, remotely or locally. */ data object Canceled : State /** Verification successful. */ data object Completed : State + + data object Exit : State } sealed interface Event { - /** User wants to use another session. */ - data object UseAnotherDevice : Event - /** Request verification. */ - data object RequestVerification : Event + data class RequestVerification(val verificationRequest: VerificationRequest.Outgoing) : Event /** The current verification request has been accepted. */ data object DidAcceptVerificationRequest : Event diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/VerifySelfSessionStateProvider.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/VerifySelfSessionStateProvider.kt index 9ebb80b68b..ec3e1c29e2 100644 --- a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/VerifySelfSessionStateProvider.kt +++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/VerifySelfSessionStateProvider.kt @@ -11,18 +11,36 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.features.verifysession.impl.outgoing.VerifySelfSessionState.Step import io.element.android.features.verifysession.impl.ui.aDecimalsSessionVerificationData import io.element.android.features.verifysession.impl.ui.aEmojisSessionVerificationData -import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.verification.VerificationRequest open class VerifySelfSessionStateProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( - aVerifySelfSessionState(displaySkipButton = true), aVerifySelfSessionState( - step = Step.AwaitingOtherDeviceResponse + step = Step.Initial, + request = anOutgoingSessionVerificationRequest(), ), aVerifySelfSessionState( - step = Step.Verifying(aEmojisSessionVerificationData(), AsyncData.Uninitialized) + step = Step.Initial, + request = anOutgoingUserVerificationRequest(), + ), + aVerifySelfSessionState( + step = Step.AwaitingOtherDeviceResponse, + request = anOutgoingSessionVerificationRequest(), + ), + aVerifySelfSessionState( + step = Step.AwaitingOtherDeviceResponse, + request = anOutgoingUserVerificationRequest(), + ), + aVerifySelfSessionState( + step = Step.Verifying(aEmojisSessionVerificationData(), AsyncData.Uninitialized), + request = anOutgoingSessionVerificationRequest(), + ), + aVerifySelfSessionState( + step = Step.Verifying(aEmojisSessionVerificationData(), AsyncData.Uninitialized), + request = anOutgoingUserVerificationRequest(), ), aVerifySelfSessionState( step = Step.Verifying(aEmojisSessionVerificationData(), AsyncData.Loading()) @@ -37,40 +55,32 @@ open class VerifySelfSessionStateProvider : PreviewParameterProvider = AsyncAction.Uninitialized, - displaySkipButton: Boolean = false, + step: Step = Step.Initial, + request: VerificationRequest.Outgoing = anOutgoingSessionVerificationRequest(), eventSink: (VerifySelfSessionViewEvents) -> Unit = {}, ) = VerifySelfSessionState( step = step, - displaySkipButton = displaySkipButton, + request = request, eventSink = eventSink, - signOutAction = signOutAction, ) diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/VerifySelfSessionView.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/VerifySelfSessionView.kt index 2a73a4b627..05914ba73d 100644 --- a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/VerifySelfSessionView.kt +++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/VerifySelfSessionView.kt @@ -17,12 +17,8 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp @@ -32,22 +28,21 @@ import io.element.android.features.verifysession.impl.R import io.element.android.features.verifysession.impl.outgoing.VerifySelfSessionState.Step import io.element.android.features.verifysession.impl.ui.VerificationBottomMenu import io.element.android.features.verifysession.impl.ui.VerificationContentVerifying -import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage import io.element.android.libraries.designsystem.components.BigIcon import io.element.android.libraries.designsystem.components.PageTitle -import io.element.android.libraries.designsystem.components.ProgressDialog +import io.element.android.libraries.designsystem.components.button.BackButton 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.CircularProgressIndicator import io.element.android.libraries.designsystem.theme.components.InvisibleButton -import io.element.android.libraries.designsystem.theme.components.OutlinedButton import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.TextButton import io.element.android.libraries.designsystem.theme.components.TopAppBar import io.element.android.libraries.matrix.api.verification.SessionVerificationData +import io.element.android.libraries.matrix.api.verification.VerificationRequest import io.element.android.libraries.ui.strings.CommonStrings @OptIn(ExperimentalMaterial3Api::class) @@ -55,18 +50,16 @@ import io.element.android.libraries.ui.strings.CommonStrings fun VerifySelfSessionView( state: VerifySelfSessionState, onLearnMoreClick: () -> Unit, - onEnterRecoveryKey: () -> Unit, - onResetKey: () -> Unit, onFinish: () -> Unit, + onBack: () -> Unit, modifier: Modifier = Modifier, ) { val step = state.step fun cancelOrResetFlow() { when (step) { is Step.Canceled -> state.eventSink(VerifySelfSessionViewEvents.Reset) - is Step.AwaitingOtherDeviceResponse, - Step.UseAnotherDevice, - Step.Ready -> state.eventSink(VerifySelfSessionViewEvents.Cancel) + Step.Initial, Step.Completed -> onBack() + Step.Ready, is Step.AwaitingOtherDeviceResponse -> state.eventSink(VerifySelfSessionViewEvents.Cancel) is Step.Verifying -> { if (!step.state.isLoading()) { state.eventSink(VerifySelfSessionViewEvents.DeclineVerification) @@ -76,18 +69,11 @@ fun VerifySelfSessionView( } } - val latestOnFinish by rememberUpdatedState(newValue = onFinish) - LaunchedEffect(step, latestOnFinish) { - if (step is Step.Skipped) { - latestOnFinish() - } - } BackHandler { cancelOrResetFlow() } - if (step is Step.Loading || - step is Step.Skipped) { + if (step is Step.Loading) { // Just display a loader in this case, to avoid UI glitch. Box( modifier = Modifier.fillMaxSize(), @@ -101,94 +87,94 @@ fun VerifySelfSessionView( topBar = { TopAppBar( title = {}, - actions = { - if (step !is Step.Completed && - state.displaySkipButton && - LocalInspectionMode.current.not()) { - TextButton( - text = stringResource(CommonStrings.action_skip), - onClick = { state.eventSink(VerifySelfSessionViewEvents.SkipVerification) } - ) - } - if (step is Step.Initial) { - TextButton( - text = stringResource(CommonStrings.action_signout), - onClick = { state.eventSink(VerifySelfSessionViewEvents.SignOut) } - ) - } + navigationIcon = if (step != Step.Completed) { + { BackButton(onClick = ::cancelOrResetFlow) } + } else { + {} } ) }, header = { - VerifySelfSessionHeader(step = step) + VerifySelfSessionHeader(step = step, request = state.request) }, footer = { VerifySelfSessionBottomMenu( screenState = state, onCancelClick = ::cancelOrResetFlow, - onEnterRecoveryKey = onEnterRecoveryKey, onContinueClick = onFinish, - onResetKey = onResetKey, ) } ) { VerifySelfSessionContent( flowState = step, + request = state.request, onLearnMoreClick = onLearnMoreClick, ) } } - - when (state.signOutAction) { - AsyncAction.Loading -> { - ProgressDialog(text = stringResource(id = R.string.screen_signout_in_progress_dialog_content)) - } - is AsyncAction.Success, - is AsyncAction.Confirming, - is AsyncAction.Failure, - AsyncAction.Uninitialized -> Unit - } } @Composable -private fun VerifySelfSessionHeader(step: Step) { +private fun VerifySelfSessionHeader(step: Step, request: VerificationRequest.Outgoing) { val iconStyle = when (step) { Step.Loading -> error("Should not happen") - is Step.Initial -> BigIcon.Style.Default(CompoundIcons.LockSolid()) - Step.UseAnotherDevice -> BigIcon.Style.Default(CompoundIcons.Devices()) - Step.AwaitingOtherDeviceResponse -> BigIcon.Style.Default(CompoundIcons.Devices()) + Step.Initial -> when (request) { + is VerificationRequest.Outgoing.CurrentSession -> BigIcon.Style.Default(CompoundIcons.Devices()) + is VerificationRequest.Outgoing.User -> BigIcon.Style.Default(CompoundIcons.LockSolid()) + } + Step.AwaitingOtherDeviceResponse -> BigIcon.Style.Loading Step.Canceled -> BigIcon.Style.AlertSolid - Step.Ready, is Step.Verifying -> BigIcon.Style.Default(CompoundIcons.Reaction()) + Step.Ready -> BigIcon.Style.Default(CompoundIcons.Reaction()) Step.Completed -> BigIcon.Style.SuccessSolid - is Step.Skipped -> return + is Step.Verifying -> { + if (step.state is AsyncData.Loading) { + BigIcon.Style.Loading + } else { + BigIcon.Style.Default(CompoundIcons.Reaction()) + } + } + is Step.Exit -> return } val titleTextId = when (step) { Step.Loading -> error("Should not happen") - is Step.Initial -> R.string.screen_identity_confirmation_title - Step.UseAnotherDevice -> R.string.screen_session_verification_use_another_device_title - Step.AwaitingOtherDeviceResponse -> R.string.screen_session_verification_waiting_another_device_title + Step.Initial -> when (request) { + is VerificationRequest.Outgoing.CurrentSession -> R.string.screen_session_verification_use_another_device_title + is VerificationRequest.Outgoing.User -> R.string.screen_session_verification_user_initiator_title + } + Step.AwaitingOtherDeviceResponse -> when (request) { + is VerificationRequest.Outgoing.CurrentSession -> R.string.screen_session_verification_waiting_another_device_title + is VerificationRequest.Outgoing.User -> R.string.screen_session_verification_waiting_other_user_title + } Step.Canceled -> CommonStrings.common_verification_failed Step.Ready -> R.string.screen_session_verification_compare_emojis_title - Step.Completed -> R.string.screen_identity_confirmed_title + Step.Completed -> CommonStrings.common_verification_complete is Step.Verifying -> when (step.data) { is SessionVerificationData.Decimals -> R.string.screen_session_verification_compare_numbers_title is SessionVerificationData.Emojis -> R.string.screen_session_verification_compare_emojis_title } - is Step.Skipped -> return + is Step.Exit -> return } val subtitleTextId = when (step) { Step.Loading -> error("Should not happen") - is Step.Initial -> R.string.screen_identity_confirmation_subtitle - Step.UseAnotherDevice -> R.string.screen_session_verification_use_another_device_subtitle - Step.AwaitingOtherDeviceResponse -> R.string.screen_session_verification_waiting_another_device_subtitle + Step.Initial -> when (request) { + is VerificationRequest.Outgoing.CurrentSession -> R.string.screen_session_verification_use_another_device_subtitle + is VerificationRequest.Outgoing.User -> R.string.screen_session_verification_user_initiator_subtitle + } + Step.AwaitingOtherDeviceResponse -> R.string.screen_session_verification_waiting_subtitle Step.Canceled -> R.string.screen_session_verification_failed_subtitle Step.Ready -> R.string.screen_session_verification_ready_subtitle - Step.Completed -> R.string.screen_identity_confirmed_subtitle + Step.Completed -> when (request) { + is VerificationRequest.Outgoing.CurrentSession -> R.string.screen_identity_confirmed_subtitle + is VerificationRequest.Outgoing.User -> R.string.screen_session_verification_complete_user_subtitle + } is Step.Verifying -> when (step.data) { is SessionVerificationData.Decimals -> R.string.screen_session_verification_compare_numbers_subtitle - is SessionVerificationData.Emojis -> R.string.screen_session_verification_compare_emojis_subtitle + is SessionVerificationData.Emojis -> when (request) { + is VerificationRequest.Outgoing.CurrentSession -> R.string.screen_session_verification_compare_emojis_subtitle + is VerificationRequest.Outgoing.User -> R.string.screen_session_verification_compare_emojis_user_subtitle + } } - is Step.Skipped -> return + is Step.Exit -> return } PageTitle( @@ -201,11 +187,15 @@ private fun VerifySelfSessionHeader(step: Step) { @Composable private fun VerifySelfSessionContent( flowState: Step, + request: VerificationRequest.Outgoing, onLearnMoreClick: () -> Unit, ) { when (flowState) { is Step.Initial -> { - ContentInitial(onLearnMoreClick) + when (request) { + is VerificationRequest.Outgoing.CurrentSession -> Unit + is VerificationRequest.Outgoing.User -> ContentInitial(onLearnMoreClick) + } } is Step.Verifying -> { VerificationContentVerifying(flowState.data) @@ -235,8 +225,6 @@ private fun ContentInitial( @Composable private fun VerifySelfSessionBottomMenu( screenState: VerifySelfSessionState, - onEnterRecoveryKey: () -> Unit, - onResetKey: () -> Unit, onCancelClick: () -> Unit, onContinueClick: () -> Unit, ) { @@ -248,27 +236,6 @@ private fun VerifySelfSessionBottomMenu( when (verificationViewState) { Step.Loading -> error("Should not happen") is Step.Initial -> { - VerificationBottomMenu { - if (verificationViewState.isLastDevice.not()) { - Button( - modifier = Modifier.fillMaxWidth(), - text = stringResource(R.string.screen_identity_use_another_device), - onClick = { eventSink(VerifySelfSessionViewEvents.UseAnotherDevice) }, - ) - } - Button( - modifier = Modifier.fillMaxWidth(), - text = stringResource(R.string.screen_session_verification_enter_recovery_key), - onClick = onEnterRecoveryKey, - ) - OutlinedButton( - modifier = Modifier.fillMaxWidth(), - text = stringResource(R.string.screen_identity_confirmation_cannot_confirm), - onClick = onResetKey, - ) - } - } - is Step.UseAnotherDevice -> { VerificationBottomMenu { Button( modifier = Modifier.fillMaxWidth(), @@ -302,39 +269,20 @@ private fun VerifySelfSessionBottomMenu( ) } } - is Step.AwaitingOtherDeviceResponse -> { - VerificationBottomMenu { - Button( - modifier = Modifier.fillMaxWidth(), - text = stringResource(R.string.screen_identity_waiting_on_other_device), - onClick = {}, - showProgress = true, - enabled = false, - ) - InvisibleButton() - } - } + is Step.AwaitingOtherDeviceResponse -> Unit is Step.Verifying -> { - val positiveButtonTitle = if (isVerifying) { - stringResource(R.string.screen_session_verification_positive_button_verifying_ongoing) + if (isVerifying) { + // Show nothing } else { - stringResource(R.string.screen_session_verification_they_match) - } - VerificationBottomMenu { - Button( - modifier = Modifier.fillMaxWidth(), - text = positiveButtonTitle, - showProgress = isVerifying, - enabled = !isVerifying, - onClick = { - if (!isVerifying) { + VerificationBottomMenu { + Button( + modifier = Modifier.fillMaxWidth(), + text = stringResource(R.string.screen_session_verification_they_match), + onClick = { eventSink(VerifySelfSessionViewEvents.ConfirmVerification) - } - }, - ) - if (isVerifying) { - InvisibleButton() - } else { + }, + ) + TextButton( modifier = Modifier.fillMaxWidth(), text = stringResource(R.string.screen_session_verification_they_dont_match), @@ -353,7 +301,7 @@ private fun VerifySelfSessionBottomMenu( InvisibleButton() } } - is Step.Skipped -> return + is Step.Exit -> return } } @@ -363,8 +311,7 @@ internal fun VerifySelfSessionViewPreview(@PreviewParameter(VerifySelfSessionSta VerifySelfSessionView( state = state, onLearnMoreClick = {}, - onEnterRecoveryKey = {}, - onResetKey = {}, onFinish = {}, + onBack = {}, ) } diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/VerifySelfSessionViewEvents.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/VerifySelfSessionViewEvents.kt index 752dbc3d7a..281991cb2d 100644 --- a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/VerifySelfSessionViewEvents.kt +++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/VerifySelfSessionViewEvents.kt @@ -8,13 +8,10 @@ package io.element.android.features.verifysession.impl.outgoing sealed interface VerifySelfSessionViewEvents { - data object UseAnotherDevice : VerifySelfSessionViewEvents data object RequestVerification : VerifySelfSessionViewEvents data object StartSasVerification : VerifySelfSessionViewEvents data object ConfirmVerification : VerifySelfSessionViewEvents data object DeclineVerification : VerifySelfSessionViewEvents data object Cancel : VerifySelfSessionViewEvents data object Reset : VerifySelfSessionViewEvents - data object SignOut : VerifySelfSessionViewEvents - data object SkipVerification : VerifySelfSessionViewEvents } diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/ui/VerificationUserProfileContent.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/ui/VerificationUserProfileContent.kt new file mode 100644 index 0000000000..3b10499b39 --- /dev/null +++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/ui/VerificationUserProfileContent.kt @@ -0,0 +1,73 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.verifysession.impl.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.unit.dp +import io.element.android.compound.theme.ElementTheme +import io.element.android.libraries.designsystem.components.avatar.Avatar +import io.element.android.libraries.designsystem.components.avatar.AvatarData +import io.element.android.libraries.designsystem.components.avatar.AvatarSize +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.matrix.api.core.UserId + +@Composable +fun VerificationUserProfileContent( + userId: UserId, + displayName: String?, + avatarUrl: String?, + modifier: Modifier = Modifier, +) { + val avatarData = remember(userId, displayName, avatarUrl) { + AvatarData(id = userId.value, name = displayName, url = avatarUrl, size = AvatarSize.UserVerification) + } + + Row( + modifier = modifier.fillMaxWidth() + .clip(RoundedCornerShape(8.dp)) + .background(ElementTheme.colors.bgSubtleSecondary) + .padding(12.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Avatar(avatarData) + + Spacer(modifier = Modifier.padding(12.dp)) + + Column(verticalArrangement = Arrangement.spacedBy(2.dp)) { + Text(text = displayName ?: userId.value, style = ElementTheme.typography.fontBodyLgMedium, color = ElementTheme.colors.textPrimary) + + if (displayName != null) { + Text(text = userId.value, style = ElementTheme.typography.fontBodyMdRegular, color = ElementTheme.colors.textSecondary) + } + } + } +} + +@PreviewsDayNight +@Composable +internal fun VerificationUserProfileContentPreview() = ElementPreview { + VerificationUserProfileContent( + userId = UserId("@alice:example.com"), + displayName = "Alice", + avatarUrl = "https://example.com/avatar.png", + ) +} diff --git a/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationPresenterTest.kt b/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationPresenterTest.kt index 74b62a5654..6e866b13af 100644 --- a/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationPresenterTest.kt +++ b/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationPresenterTest.kt @@ -15,6 +15,7 @@ import io.element.android.libraries.matrix.api.core.FlowId 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.matrix.test.A_DEVICE_ID import io.element.android.libraries.matrix.test.A_TIMESTAMP import io.element.android.libraries.matrix.test.A_USER_ID @@ -25,6 +26,7 @@ import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.value import io.element.android.tests.testutils.test import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest import org.junit.Rule @@ -37,7 +39,7 @@ class IncomingVerificationPresenterTest { @Test fun `present - nominal case - incoming verification successful`() = runTest { - val acknowledgeVerificationRequestLambda = lambdaRecorder { _ -> } + val acknowledgeVerificationRequestLambda = lambdaRecorder { _ -> } val acceptVerificationRequestLambda = lambdaRecorder { } val approveVerificationLambda = lambdaRecorder { } val resetLambda = lambdaRecorder { } @@ -60,7 +62,7 @@ class IncomingVerificationPresenterTest { ) ) resetLambda.assertions().isCalledOnce().with(value(false)) - acknowledgeVerificationRequestLambda.assertions().isCalledOnce().with(value(aSessionVerificationRequestDetails)) + acknowledgeVerificationRequestLambda.assertions().isCalledOnce().with(value(anIncomingSessionVerificationRequest)) acceptVerificationRequestLambda.assertions().isNeverCalled() // User accept the incoming verification initialState.eventSink(IncomingVerificationViewEvents.StartVerification) @@ -100,7 +102,7 @@ class IncomingVerificationPresenterTest { @Test fun `present - emoji not matching case - incoming verification failure`() = runTest { - val acknowledgeVerificationRequestLambda = lambdaRecorder { _ -> } + val acknowledgeVerificationRequestLambda = lambdaRecorder { _ -> } val acceptVerificationRequestLambda = lambdaRecorder { } val declineVerificationLambda = lambdaRecorder { } val resetLambda = lambdaRecorder { } @@ -123,7 +125,7 @@ class IncomingVerificationPresenterTest { ) ) resetLambda.assertions().isCalledOnce().with(value(false)) - acknowledgeVerificationRequestLambda.assertions().isCalledOnce().with(value(aSessionVerificationRequestDetails)) + acknowledgeVerificationRequestLambda.assertions().isCalledOnce().with(value(anIncomingSessionVerificationRequest)) acceptVerificationRequestLambda.assertions().isNeverCalled() // User accept the incoming verification initialState.eventSink(IncomingVerificationViewEvents.StartVerification) @@ -157,7 +159,7 @@ class IncomingVerificationPresenterTest { @Test fun `present - incoming verification is remotely canceled`() = runTest { - val acknowledgeVerificationRequestLambda = lambdaRecorder { _ -> } + val acknowledgeVerificationRequestLambda = lambdaRecorder { _ -> } val acceptVerificationRequestLambda = lambdaRecorder { } val declineVerificationLambda = lambdaRecorder { } val resetLambda = lambdaRecorder { } @@ -191,7 +193,7 @@ class IncomingVerificationPresenterTest { @Test fun `present - user goes back when comparing emoji - incoming verification failure`() = runTest { - val acknowledgeVerificationRequestLambda = lambdaRecorder { _ -> } + val acknowledgeVerificationRequestLambda = lambdaRecorder { _ -> } val acceptVerificationRequestLambda = lambdaRecorder { } val declineVerificationLambda = lambdaRecorder { } val resetLambda = lambdaRecorder { } @@ -214,7 +216,7 @@ class IncomingVerificationPresenterTest { ) ) resetLambda.assertions().isCalledOnce().with(value(false)) - acknowledgeVerificationRequestLambda.assertions().isCalledOnce().with(value(aSessionVerificationRequestDetails)) + acknowledgeVerificationRequestLambda.assertions().isCalledOnce().with(value(anIncomingSessionVerificationRequest)) acceptVerificationRequestLambda.assertions().isNeverCalled() // User accept the incoming verification initialState.eventSink(IncomingVerificationViewEvents.StartVerification) @@ -248,7 +250,7 @@ class IncomingVerificationPresenterTest { @Test fun `present - user ignores incoming request`() = runTest { - val acknowledgeVerificationRequestLambda = lambdaRecorder { _ -> } + val acknowledgeVerificationRequestLambda = lambdaRecorder { _ -> } val acceptVerificationRequestLambda = lambdaRecorder { } val resetLambda = lambdaRecorder { } val fakeSessionVerificationService = FakeSessionVerificationService( @@ -268,24 +270,30 @@ class IncomingVerificationPresenterTest { } } - private val aSessionVerificationRequestDetails = SessionVerificationRequestDetails( - senderId = A_USER_ID, - flowId = FlowId("flowId"), - deviceId = A_DEVICE_ID, - displayName = "a device name", - firstSeenTimestamp = A_TIMESTAMP, + private val anIncomingSessionVerificationRequest = VerificationRequest.Incoming.OtherSession( + details = SessionVerificationRequestDetails( + senderProfile = SessionVerificationRequestDetails.SenderProfile( + userId = A_USER_ID, + displayName = "a device name", + avatarUrl = null, + ), + flowId = FlowId("flowId"), + deviceId = A_DEVICE_ID, + firstSeenTimestamp = A_TIMESTAMP, + ) ) - private fun createPresenter( - sessionVerificationRequestDetails: SessionVerificationRequestDetails = aSessionVerificationRequestDetails, + private fun TestScope.createPresenter( + verificationRequest: VerificationRequest.Incoming = anIncomingSessionVerificationRequest, navigator: IncomingVerificationNavigator = IncomingVerificationNavigator { lambdaError() }, service: SessionVerificationService = FakeSessionVerificationService(), dateFormatter: DateFormatter = FakeDateFormatter(), ) = IncomingVerificationPresenter( - sessionVerificationRequestDetails = sessionVerificationRequestDetails, + verificationRequest = verificationRequest, navigator = navigator, sessionVerificationService = service, stateMachine = IncomingVerificationStateMachine(service), dateFormatter = dateFormatter, + sessionCoroutineScope = backgroundScope, ) } diff --git a/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/outgoing/VerifySelfSessionPresenterTest.kt b/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/outgoing/VerifySelfSessionPresenterTest.kt index 435e2e688c..2328fc56cd 100644 --- a/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/outgoing/VerifySelfSessionPresenterTest.kt +++ b/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/outgoing/VerifySelfSessionPresenterTest.kt @@ -9,27 +9,21 @@ package io.element.android.features.verifysession.impl.outgoing import app.cash.turbine.ReceiveTurbine import com.google.common.truth.Truth.assertThat -import io.element.android.features.logout.api.LogoutUseCase -import io.element.android.features.logout.test.FakeLogoutUseCase import io.element.android.features.verifysession.impl.outgoing.VerifySelfSessionState.Step import io.element.android.libraries.architecture.AsyncData -import io.element.android.libraries.core.meta.BuildMeta +import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.encryption.EncryptionService -import io.element.android.libraries.matrix.api.encryption.RecoveryState import io.element.android.libraries.matrix.api.verification.SessionVerificationData -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.SessionVerifiedStatus import io.element.android.libraries.matrix.api.verification.VerificationEmoji import io.element.android.libraries.matrix.api.verification.VerificationFlowState -import io.element.android.libraries.matrix.test.core.aBuildMeta +import io.element.android.libraries.matrix.api.verification.VerificationRequest import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService -import io.element.android.libraries.preferences.test.InMemorySessionPreferencesStore import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.lambda.lambdaError import io.element.android.tests.testutils.lambda.lambdaRecorder -import io.element.android.tests.testutils.lambda.value import io.element.android.tests.testutils.test import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest @@ -48,85 +42,70 @@ class VerifySelfSessionPresenterTest { ) presenter.test { awaitItem().run { - assertThat(step).isEqualTo(Step.Initial(false)) - assertThat(displaySkipButton).isTrue() + assertThat(step).isEqualTo(Step.Initial) } } } @Test - fun `present - hides skip verification button on non-debuggable builds`() = runTest { - val buildMeta = aBuildMeta(isDebuggable = false) - val presenter = createVerifySelfSessionPresenter( - service = unverifiedSessionService(), - buildMeta = buildMeta, - ) - presenter.test { - assertThat(awaitItem().displaySkipButton).isFalse() - } - } - - @Test - fun `present - Initial state is received, can use recovery key`() = runTest { - val resetLambda = lambdaRecorder { } - val presenter = createVerifySelfSessionPresenter( - service = unverifiedSessionService( - resetLambda = resetLambda - ), - encryptionService = FakeEncryptionService().apply { - emitRecoveryState(RecoveryState.INCOMPLETE) - } - ) - presenter.test { - assertThat(awaitItem().step).isEqualTo(Step.Initial(true)) - resetLambda.assertions().isCalledOnce().with(value(true)) - } - } - - @Test - fun `present - Initial state is received, can use recovery key and is last device`() = runTest { - val presenter = createVerifySelfSessionPresenter( - service = unverifiedSessionService(), - encryptionService = FakeEncryptionService().apply { - emitIsLastDevice(true) - emitRecoveryState(RecoveryState.INCOMPLETE) - } - ) - presenter.test { - assertThat(awaitItem().step).isEqualTo(Step.Initial(canEnterRecoveryKey = true, isLastDevice = true)) - } - } - - @Test - fun `present - Handles requestVerification`() = runTest { + fun `present - Handles requestVerification for session verification`() = runTest { + val requestSessionVerificationRecorder = lambdaRecorder {} + val startVerificationRecorder = lambdaRecorder {} val service = unverifiedSessionService( - requestVerificationLambda = { }, - startVerificationLambda = { }, + requestSessionVerificationLambda = requestSessionVerificationRecorder, + startVerificationLambda = startVerificationRecorder, + ) + val presenter = createVerifySelfSessionPresenter( + service = service, + verificationRequest = anOutgoingSessionVerificationRequest(), ) - val presenter = createVerifySelfSessionPresenter(service) presenter.test { requestVerificationAndAwaitVerifyingState(service) + + requestSessionVerificationRecorder.assertions().isCalledOnce() + startVerificationRecorder.assertions().isCalledOnce() } } @Test - fun `present - Cancellation on initial state does nothing`() = runTest { + fun `present - Handles requestVerification for user verification`() = runTest { + val requestUserVerificationRecorder = lambdaRecorder {} + val startVerificationRecorder = lambdaRecorder {} + val service = unverifiedSessionService( + requestUserVerificationLambda = requestUserVerificationRecorder, + startVerificationLambda = startVerificationRecorder, + ) + val presenter = createVerifySelfSessionPresenter( + service = service, + verificationRequest = anOutgoingUserVerificationRequest(), + ) + presenter.test { + requestVerificationAndAwaitVerifyingState(service) + + requestUserVerificationRecorder.assertions().isCalledOnce() + startVerificationRecorder.assertions().isCalledOnce() + } + } + + @Test + fun `present - Cancellation on initial state moves to Exit state`() = runTest { val presenter = createVerifySelfSessionPresenter( service = unverifiedSessionService(), ) presenter.test { val initialState = awaitItem() - assertThat(initialState.step).isEqualTo(Step.Initial(false)) + assertThat(initialState.step).isEqualTo(Step.Initial) val eventSink = initialState.eventSink eventSink(VerifySelfSessionViewEvents.Cancel) - expectNoEvents() + + assertThat(awaitItem().step).isEqualTo(Step.Exit) } } @Test fun `present - A failure when verifying cancels it`() = runTest { val service = unverifiedSessionService( - requestVerificationLambda = { }, + requestSessionVerificationLambda = { }, startVerificationLambda = { }, approveVerificationLambda = { }, ) @@ -143,24 +122,23 @@ class VerifySelfSessionPresenterTest { } @Test - fun `present - A fail when requesting verification resets the state to the initial one`() = runTest { + fun `present - A fail when requesting verification resets the state to the canceled one`() = runTest { val service = unverifiedSessionService( - requestVerificationLambda = { }, + requestSessionVerificationLambda = { }, ) val presenter = createVerifySelfSessionPresenter(service) presenter.test { - awaitItem().eventSink(VerifySelfSessionViewEvents.UseAnotherDevice) awaitItem().eventSink(VerifySelfSessionViewEvents.RequestVerification) service.emitVerificationFlowState(VerificationFlowState.DidFail) assertThat(awaitItem().step).isInstanceOf(Step.AwaitingOtherDeviceResponse::class.java) - assertThat(awaitItem().step).isEqualTo(Step.Initial(false)) + assertThat(awaitItem().step).isEqualTo(Step.Canceled) } } @Test fun `present - Canceling the flow once it's verifying cancels it`() = runTest { val service = unverifiedSessionService( - requestVerificationLambda = { }, + requestSessionVerificationLambda = { }, startVerificationLambda = { }, cancelVerificationLambda = { }, ) @@ -175,7 +153,7 @@ class VerifySelfSessionPresenterTest { @Test fun `present - When verifying, if we receive another challenge we ignore it`() = runTest { val service = unverifiedSessionService( - requestVerificationLambda = { }, + requestSessionVerificationLambda = { }, startVerificationLambda = { }, ) val presenter = createVerifySelfSessionPresenter(service) @@ -189,7 +167,7 @@ class VerifySelfSessionPresenterTest { @Test fun `present - Go back after cancellation returns to initial state`() = runTest { val service = unverifiedSessionService( - requestVerificationLambda = { }, + requestSessionVerificationLambda = { }, startVerificationLambda = { }, ) val presenter = createVerifySelfSessionPresenter(service) @@ -199,7 +177,7 @@ class VerifySelfSessionPresenterTest { assertThat(awaitItem().step).isEqualTo(Step.Canceled) state.eventSink(VerifySelfSessionViewEvents.Reset) // Went back to initial state - assertThat(awaitItem().step).isEqualTo(Step.Initial(false)) + assertThat(awaitItem().step).isEqualTo(Step.Initial) cancelAndIgnoreRemainingEvents() } } @@ -210,7 +188,7 @@ class VerifySelfSessionPresenterTest { VerificationEmoji(number = 30, emoji = "😀", description = "Smiley") ) val service = unverifiedSessionService( - requestVerificationLambda = { }, + requestSessionVerificationLambda = { }, startVerificationLambda = { }, approveVerificationLambda = { }, ) @@ -235,7 +213,7 @@ class VerifySelfSessionPresenterTest { @Test fun `present - When verification is declined, the flow is canceled`() = runTest { val service = unverifiedSessionService( - requestVerificationLambda = { }, + requestSessionVerificationLambda = { }, startVerificationLambda = { }, declineVerificationLambda = { }, ) @@ -254,20 +232,6 @@ class VerifySelfSessionPresenterTest { } } - @Test - fun `present - Skip event skips the flow`() = runTest { - val service = unverifiedSessionService( - requestVerificationLambda = { }, - startVerificationLambda = { }, - ) - val presenter = createVerifySelfSessionPresenter(service) - presenter.test { - val state = requestVerificationAndAwaitVerifyingState(service) - state.eventSink(VerifySelfSessionViewEvents.SkipVerification) - assertThat(awaitItem().step).isEqualTo(Step.Skipped) - } - } - @Test fun `present - When verification is done using recovery key, the flow is completed`() = runTest { val service = FakeSessionVerificationService( @@ -301,32 +265,7 @@ class VerifySelfSessionPresenterTest { ) presenter.test { skipItems(1) - assertThat(awaitItem().step).isEqualTo(Step.Skipped) - } - } - - @Test - fun `present - When user request to sign out, the sign out use case is invoked`() = runTest { - val service = FakeSessionVerificationService( - resetLambda = { }, - ).apply { - emitNeedsSessionVerification(false) - emitVerifiedStatus(SessionVerifiedStatus.Verified) - emitVerificationFlowState(VerificationFlowState.DidFinish) - } - val signOutLambda = lambdaRecorder {} - val presenter = createVerifySelfSessionPresenter( - service, - logoutUseCase = FakeLogoutUseCase(signOutLambda) - ) - presenter.test { - skipItems(1) - val initialItem = awaitItem() - initialItem.eventSink(VerifySelfSessionViewEvents.SignOut) - assertThat(awaitItem().signOutAction.isLoading()).isTrue() - val finalItem = awaitItem() - assertThat(finalItem.signOutAction.isSuccess()).isTrue() - signOutLambda.assertions().isCalledOnce().with(value(true)) + assertThat(awaitItem().step).isEqualTo(Step.Exit) } } @@ -335,10 +274,7 @@ class VerifySelfSessionPresenterTest { sessionVerificationData: SessionVerificationData = SessionVerificationData.Emojis(emptyList()), ): VerifySelfSessionState { var state = awaitItem() - assertThat(state.step).isEqualTo(Step.Initial(false)) - state.eventSink(VerifySelfSessionViewEvents.UseAnotherDevice) - state = awaitItem() - assertThat(state.step).isEqualTo(Step.UseAnotherDevice) + assertThat(state.step).isEqualTo(Step.Initial) state.eventSink(VerifySelfSessionViewEvents.RequestVerification) // Await for other device response: fakeService.emitVerificationFlowState(VerificationFlowState.DidAcceptVerificationRequest) @@ -360,17 +296,19 @@ class VerifySelfSessionPresenterTest { } private suspend fun unverifiedSessionService( - requestVerificationLambda: () -> Unit = { lambdaError() }, + requestSessionVerificationLambda: () -> Unit = { lambdaError() }, + requestUserVerificationLambda: (UserId) -> Unit = { lambdaError() }, cancelVerificationLambda: () -> Unit = { lambdaError() }, approveVerificationLambda: () -> Unit = { lambdaError() }, declineVerificationLambda: () -> Unit = { lambdaError() }, startVerificationLambda: () -> Unit = { lambdaError() }, resetLambda: (Boolean) -> Unit = { }, - acknowledgeVerificationRequestLambda: (SessionVerificationRequestDetails) -> Unit = { lambdaError() }, + acknowledgeVerificationRequestLambda: (VerificationRequest.Incoming) -> Unit = { lambdaError() }, acceptVerificationRequestLambda: () -> Unit = { lambdaError() }, ): FakeSessionVerificationService { return FakeSessionVerificationService( - requestVerificationLambda = requestVerificationLambda, + requestCurrentSessionVerificationLambda = requestSessionVerificationLambda, + requestUserVerificationLambda = requestUserVerificationLambda, cancelVerificationLambda = cancelVerificationLambda, approveVerificationLambda = approveVerificationLambda, declineVerificationLambda = declineVerificationLambda, @@ -385,20 +323,15 @@ class VerifySelfSessionPresenterTest { private fun createVerifySelfSessionPresenter( service: SessionVerificationService, + verificationRequest: VerificationRequest.Outgoing = anOutgoingSessionVerificationRequest(), encryptionService: EncryptionService = FakeEncryptionService(), - buildMeta: BuildMeta = aBuildMeta(), - sessionPreferencesStore: InMemorySessionPreferencesStore = InMemorySessionPreferencesStore(), - logoutUseCase: LogoutUseCase = FakeLogoutUseCase(), showDeviceVerifiedScreen: Boolean = false, ): VerifySelfSessionPresenter { return VerifySelfSessionPresenter( showDeviceVerifiedScreen = showDeviceVerifiedScreen, + verificationRequest = verificationRequest, sessionVerificationService = service, encryptionService = encryptionService, - stateMachine = VerifySelfSessionStateMachine(service, encryptionService), - buildMeta = buildMeta, - sessionPreferencesStore = sessionPreferencesStore, - logoutUseCase = logoutUseCase, ) } } diff --git a/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/outgoing/VerifySelfSessionViewTest.kt b/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/outgoing/VerifySelfSessionViewTest.kt index 9eb8724152..c24809c31e 100644 --- a/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/outgoing/VerifySelfSessionViewTest.kt +++ b/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/outgoing/VerifySelfSessionViewTest.kt @@ -16,7 +16,6 @@ import io.element.android.features.verifysession.impl.ui.aEmojisSessionVerificat import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.tests.testutils.EnsureNeverCalled -import io.element.android.tests.testutils.EnsureNeverCalledWithParam import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.ensureCalledOnce @@ -25,7 +24,6 @@ import org.junit.Rule import org.junit.Test import org.junit.rules.TestRule import org.junit.runner.RunWith -import org.robolectric.annotation.Config @RunWith(AndroidJUnit4::class) class VerifySelfSessionViewTest { @@ -103,16 +101,16 @@ class VerifySelfSessionViewTest { } @Test - fun `back key pressed - on Completed step does nothing`() { - val eventsRecorder = EventsRecorder() - rule.setVerifySelfSessionView( - aVerifySelfSessionState( - step = VerifySelfSessionState.Step.Completed, - eventSink = eventsRecorder - ), - ) - rule.pressBackKey() - eventsRecorder.assertEmpty() + fun `back key pressed - on Completed exits the flow`() { + ensureCalledOnce { callback -> + rule.setVerifySelfSessionView( + onBack = callback, + state = aVerifySelfSessionState( + step = VerifySelfSessionState.Step.Completed, + ), + ) + rule.pressBackKey() + } } @Test @@ -130,38 +128,6 @@ class VerifySelfSessionViewTest { } } - @Config(qualifiers = "h1024dp") - @Test - fun `clicking on enter recovery key calls the expected callback`() { - val eventsRecorder = EventsRecorder(expectEvents = false) - ensureCalledOnce { callback -> - rule.setVerifySelfSessionView( - aVerifySelfSessionState( - step = VerifySelfSessionState.Step.Initial(true), - eventSink = eventsRecorder - ), - onEnterRecoveryKey = callback, - ) - rule.clickOn(R.string.screen_session_verification_enter_recovery_key) - } - } - - @Config(qualifiers = "h1024dp") - @Test - fun `clicking on learn more invokes the expected callback`() { - val eventsRecorder = EventsRecorder(expectEvents = false) - ensureCalledOnce { callback -> - rule.setVerifySelfSessionView( - aVerifySelfSessionState( - step = VerifySelfSessionState.Step.Initial(true), - eventSink = eventsRecorder - ), - onLearnMoreClick = callback, - ) - rule.clickOn(CommonStrings.action_learn_more) - } - } - @Test fun `clicking on they match emits the expected event`() { val eventsRecorder = EventsRecorder() @@ -194,48 +160,18 @@ class VerifySelfSessionViewTest { eventsRecorder.assertSingle(VerifySelfSessionViewEvents.DeclineVerification) } - @Test - fun `clicking on 'Skip' emits the expected event`() { - val eventsRecorder = EventsRecorder() - rule.setVerifySelfSessionView( - aVerifySelfSessionState( - step = VerifySelfSessionState.Step.Initial(canEnterRecoveryKey = true), - displaySkipButton = true, - eventSink = eventsRecorder - ), - ) - rule.clickOn(CommonStrings.action_skip) - eventsRecorder.assertSingle(VerifySelfSessionViewEvents.SkipVerification) - } - - @Test - fun `on Skipped step - onFinished callback is called immediately`() { - ensureCalledOnce { callback -> - rule.setVerifySelfSessionView( - aVerifySelfSessionState( - step = VerifySelfSessionState.Step.Skipped, - displaySkipButton = true, - eventSink = EnsureNeverCalledWithParam(), - ), - onFinished = callback, - ) - } - } - private fun AndroidComposeTestRule.setVerifySelfSessionView( state: VerifySelfSessionState, onLearnMoreClick: () -> Unit = EnsureNeverCalled(), - onEnterRecoveryKey: () -> Unit = EnsureNeverCalled(), onFinished: () -> Unit = EnsureNeverCalled(), - onResetKey: () -> Unit = EnsureNeverCalled(), + onBack: () -> Unit = EnsureNeverCalled(), ) { setContent { VerifySelfSessionView( state = state, onLearnMoreClick = onLearnMoreClick, - onEnterRecoveryKey = onEnterRecoveryKey, onFinish = onFinished, - onResetKey = onResetKey, + onBack = onBack, ) } } diff --git a/libraries/designsystem/build.gradle.kts b/libraries/designsystem/build.gradle.kts index bbb8efdadf..d95566ebb5 100644 --- a/libraries/designsystem/build.gradle.kts +++ b/libraries/designsystem/build.gradle.kts @@ -32,6 +32,7 @@ android { implementation(libs.coil.compose) implementation(libs.vanniktech.blurhash) implementation(projects.features.enterprise.api) + implementation(projects.libraries.androidutils) implementation(projects.libraries.architecture) implementation(projects.libraries.core) implementation(projects.libraries.preferences.api) diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/BigIcon.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/BigIcon.kt index c9a80a50d7..f469555ee4 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/BigIcon.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/BigIcon.kt @@ -33,6 +33,7 @@ import io.element.android.compound.theme.ElementTheme import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.ui.strings.CommonStrings @@ -78,6 +79,11 @@ object BigIcon { * A success style with a tinted background. */ data object SuccessSolid : Style + + /** + * A loading style with the default background color. + */ + data object Loading : Style } /** @@ -101,31 +107,7 @@ object BigIcon { Style.Success -> Color.Transparent Style.AlertSolid -> ElementTheme.colors.bgCriticalSubtle Style.SuccessSolid -> ElementTheme.colors.bgSuccessSubtle - } - val icon = when (style) { - is Style.Default -> style.vectorIcon - Style.Alert, - Style.AlertSolid -> CompoundIcons.ErrorSolid() - Style.Success, - Style.SuccessSolid -> CompoundIcons.CheckCircleSolid() - } - val contentDescription = when (style) { - is Style.Default -> style.contentDescription - Style.Alert, - Style.AlertSolid -> stringResource(CommonStrings.common_error) - Style.Success, - Style.SuccessSolid -> stringResource(CommonStrings.common_success) - } - val iconTint = when (style) { - is Style.Default -> if (style.useCriticalTint) { - ElementTheme.colors.iconCriticalPrimary - } else { - ElementTheme.colors.iconSecondary - } - Style.Alert, - Style.AlertSolid -> ElementTheme.colors.iconCriticalPrimary - Style.Success, - Style.SuccessSolid -> ElementTheme.colors.iconSuccessPrimary + Style.Loading -> ElementTheme.colors.bgSubtleSecondary } Box( modifier = modifier @@ -134,12 +116,50 @@ object BigIcon { .background(backgroundColor), contentAlignment = Alignment.Center, ) { - Icon( - modifier = Modifier.size(32.dp), - tint = iconTint, - imageVector = icon, - contentDescription = contentDescription - ) + if (style is Style.Loading) { + CircularProgressIndicator( + modifier = Modifier.size(27.dp), + color = ElementTheme.colors.iconSecondary, + trackColor = Color.Transparent, + strokeWidth = 3.dp, + ) + } else { + val icon = when (style) { + is Style.Default -> style.vectorIcon + Style.Alert, + Style.AlertSolid -> CompoundIcons.ErrorSolid() + Style.Success, + Style.SuccessSolid -> CompoundIcons.CheckCircleSolid() + Style.Loading -> error("This should never be reached") + } + val contentDescription = when (style) { + is Style.Default -> style.contentDescription + Style.Alert, + Style.AlertSolid -> stringResource(CommonStrings.common_error) + Style.Success, + Style.SuccessSolid -> stringResource(CommonStrings.common_success) + Style.Loading -> error("This should never be reached") + } + val iconTint = when (style) { + is Style.Default -> if (style.useCriticalTint) { + ElementTheme.colors.iconCriticalPrimary + } else { + ElementTheme.colors.iconSecondary + } + Style.Alert, + Style.AlertSolid -> ElementTheme.colors.iconCriticalPrimary + Style.Success, + Style.SuccessSolid -> ElementTheme.colors.iconSuccessPrimary + Style.Loading -> error("This should never be reached") + } + + Icon( + modifier = Modifier.size(32.dp), + tint = iconTint, + imageVector = icon, + contentDescription = contentDescription + ) + } } } } @@ -173,6 +193,7 @@ internal class BigIconStyleProvider : PreviewParameterProvider { BigIcon.Style.AlertSolid, BigIcon.Style.Default(Icons.Filled.CatchingPokemon, useCriticalTint = true), BigIcon.Style.Success, - BigIcon.Style.SuccessSolid + BigIcon.Style.SuccessSolid, + BigIcon.Style.Loading, ) } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarSize.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarSize.kt index 437b36235e..0f84b82fd1 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarSize.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarSize.kt @@ -61,4 +61,6 @@ enum class AvatarSize(val dp: Dp) { MediaSender(32.dp), DmCreationConfirmation(64.dp), + + UserVerification(52.dp), } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/OpenUrlInTabView.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/OpenUrlInTabView.kt new file mode 100644 index 0000000000..9770c6849d --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/OpenUrlInTabView.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.designsystem.utils + +import androidx.activity.compose.LocalActivity +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import io.element.android.compound.theme.ElementTheme +import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab + +@Suppress("MutableStateParam") +@Composable +fun OpenUrlInTabView(url: MutableState) { + val activity = requireNotNull(LocalActivity.current) + val darkTheme = ElementTheme.isLightTheme.not() + + LaunchedEffect(url.value) { + url.value?.let { + activity.openUrlInChromeCustomTab(null, darkTheme, it) + url.value = null + } + } +} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/verification/SessionVerificationRequestDetails.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/verification/SessionVerificationRequestDetails.kt index aa4a7a4b8a..897e17c611 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/verification/SessionVerificationRequestDetails.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/verification/SessionVerificationRequestDetails.kt @@ -15,9 +15,15 @@ import kotlinx.parcelize.Parcelize @Parcelize data class SessionVerificationRequestDetails( - val senderId: UserId, + val senderProfile: SenderProfile, val flowId: FlowId, val deviceId: DeviceId, - val displayName: String?, val firstSeenTimestamp: Long, -) : Parcelable +) : Parcelable { + @Parcelize + data class SenderProfile( + val userId: UserId, + val displayName: String?, + val avatarUrl: String?, + ) : Parcelable +} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/verification/SessionVerificationService.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/verification/SessionVerificationService.kt index cf9468b493..6e34c3de32 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/verification/SessionVerificationService.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/verification/SessionVerificationService.kt @@ -8,6 +8,7 @@ package io.element.android.libraries.matrix.api.verification import androidx.compose.runtime.Immutable +import io.element.android.libraries.matrix.api.core.UserId import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow @@ -31,7 +32,12 @@ interface SessionVerificationService { /** * Request verification of the current session. */ - suspend fun requestVerification() + suspend fun requestCurrentSessionVerification() + + /** + * Request verification of the user with the given [userId]. + */ + suspend fun requestUserVerification(userId: UserId) /** * Cancels the current verification attempt. @@ -67,7 +73,7 @@ interface SessionVerificationService { * Set this particular request as the currently active one and register for * events pertaining it. */ - suspend fun acknowledgeVerificationRequest(details: SessionVerificationRequestDetails) + suspend fun acknowledgeVerificationRequest(verificationRequest: VerificationRequest.Incoming) /** * Accept the previously acknowledged verification request. @@ -76,7 +82,7 @@ interface SessionVerificationService { } interface SessionVerificationServiceListener { - fun onIncomingSessionRequest(sessionVerificationRequestDetails: SessionVerificationRequestDetails) + fun onIncomingSessionRequest(verificationRequest: VerificationRequest.Incoming) } /** Verification status of the current session. */ diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/verification/VerificationRequest.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/verification/VerificationRequest.kt new file mode 100644 index 0000000000..f26afe50f6 --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/verification/VerificationRequest.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.matrix.api.verification + +import android.os.Parcelable +import io.element.android.libraries.matrix.api.core.UserId +import kotlinx.parcelize.Parcelize + +sealed interface VerificationRequest : Parcelable { + sealed interface Outgoing : VerificationRequest { + @Parcelize + data object CurrentSession : Outgoing + + @Parcelize + data class User(val userId: UserId) : Outgoing + } + + sealed class Incoming(open val details: SessionVerificationRequestDetails) : VerificationRequest { + @Parcelize + data class OtherSession(override val details: SessionVerificationRequestDetails) : Incoming(details) + + @Parcelize + data class User(override val details: SessionVerificationRequestDetails) : Incoming(details) + } +} 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 8cd9a47234..4d48543276 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 @@ -8,13 +8,14 @@ package io.element.android.libraries.matrix.impl.verification import io.element.android.libraries.core.data.tryOrNull +import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.verification.SessionVerificationData -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.SessionVerificationServiceListener import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus import io.element.android.libraries.matrix.api.verification.VerificationEmoji import io.element.android.libraries.matrix.api.verification.VerificationFlowState +import io.element.android.libraries.matrix.api.verification.VerificationRequest import io.element.android.libraries.matrix.impl.util.cancelAndDestroy import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.NonCancellable @@ -50,6 +51,8 @@ class RustSessionVerificationService( isSyncServiceReady: Flow, private val sessionCoroutineScope: CoroutineScope, ) : SessionVerificationService, SessionVerificationControllerDelegate { + private var currentVerificationRequest: VerificationRequest? = null + private val encryptionService: Encryption = client.encryption() private lateinit var verificationController: SessionVerificationController @@ -88,10 +91,8 @@ class RustSessionVerificationService( verificationStatus == SessionVerifiedStatus.NotVerified } - private var isOwnVerification = true - override fun didReceiveVerificationRequest(details: RustSessionVerificationRequestDetails) { - listener?.onIncomingSessionRequest(details.map()) + listener?.onIncomingSessionRequest(details.toVerificationRequest(UserId(client.userId()))) } private var listener: SessionVerificationServiceListener? = null @@ -111,9 +112,16 @@ class RustSessionVerificationService( this.listener = listener } - override suspend fun requestVerification() = tryOrFail { + override suspend fun requestCurrentSessionVerification() = tryOrFail { initVerificationControllerIfNeeded() verificationController.requestDeviceVerification() + currentVerificationRequest = VerificationRequest.Outgoing.CurrentSession + } + + override suspend fun requestUserVerification(userId: UserId) = tryOrFail { + initVerificationControllerIfNeeded() + verificationController.requestUserVerification(userId.value) + currentVerificationRequest = VerificationRequest.Outgoing.User(userId) } override suspend fun cancelVerification() = tryOrFail { @@ -130,16 +138,16 @@ class RustSessionVerificationService( verificationController.startSasVerification() } - override suspend fun acknowledgeVerificationRequest(details: SessionVerificationRequestDetails) = tryOrFail { - isOwnVerification = false + override suspend fun acknowledgeVerificationRequest(verificationRequest: VerificationRequest.Incoming) = tryOrFail { initVerificationControllerIfNeeded() verificationController.acknowledgeVerificationRequest( - senderId = details.senderId.value, - flowId = details.flowId.value, + senderId = verificationRequest.details.senderProfile.userId.value, + flowId = verificationRequest.details.flowId.value, ) } override suspend fun acceptVerificationRequest() = tryOrFail { + Timber.d("Accepting incoming verification request") verificationController.acceptVerificationRequest() } @@ -183,7 +191,7 @@ class RustSessionVerificationService( } } .onSuccess { - if (isOwnVerification) { + if (currentVerificationRequest is VerificationRequest.Outgoing.CurrentSession) { // Try waiting for the final recovery state for better UX, but don't block the verification state on it tryOrNull { withTimeout(10.seconds) { @@ -215,7 +223,7 @@ class RustSessionVerificationService( // end-region override suspend fun reset(cancelAnyPendingVerificationAttempt: Boolean) { - isOwnVerification = true + currentVerificationRequest = null if (isReady.value && cancelAnyPendingVerificationAttempt) { // Cancel any pending verification attempt tryOrNull { verificationController.cancelVerification() } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/verification/SessionVerificationRequestDetails.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/verification/SessionVerificationRequestDetails.kt index b617889f40..d2e15a3fac 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/verification/SessionVerificationRequestDetails.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/verification/SessionVerificationRequestDetails.kt @@ -11,12 +11,28 @@ 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 import io.element.android.libraries.matrix.api.verification.SessionVerificationRequestDetails +import io.element.android.libraries.matrix.api.verification.VerificationRequest import org.matrix.rustcomponents.sdk.SessionVerificationRequestDetails as RustSessionVerificationRequestDetails +import org.matrix.rustcomponents.sdk.UserProfile as RustUserProfile fun RustSessionVerificationRequestDetails.map() = SessionVerificationRequestDetails( - senderId = UserId(senderProfile.userId), + senderProfile = senderProfile.map(), flowId = FlowId(flowId), deviceId = DeviceId(deviceId), - displayName = senderProfile.displayName, firstSeenTimestamp = firstSeenTimestamp.toLong(), ) + +fun RustUserProfile.map() = SessionVerificationRequestDetails.SenderProfile( + userId = UserId(userId), + displayName = displayName, + avatarUrl = avatarUrl, +) + +fun RustSessionVerificationRequestDetails.toVerificationRequest(currentUserId: UserId): VerificationRequest.Incoming { + val details = map() + return if (currentUserId == details.senderProfile.userId) { + VerificationRequest.Incoming.OtherSession(details) + } else { + VerificationRequest.Incoming.User(details) + } +} diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/verification/FakeSessionVerificationService.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/verification/FakeSessionVerificationService.kt index 65502a7cd7..d8788e4026 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/verification/FakeSessionVerificationService.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/verification/FakeSessionVerificationService.kt @@ -7,11 +7,12 @@ package io.element.android.libraries.matrix.test.verification -import io.element.android.libraries.matrix.api.verification.SessionVerificationRequestDetails +import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.verification.SessionVerificationService import io.element.android.libraries.matrix.api.verification.SessionVerificationServiceListener import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus import io.element.android.libraries.matrix.api.verification.VerificationFlowState +import io.element.android.libraries.matrix.api.verification.VerificationRequest import io.element.android.tests.testutils.lambda.lambdaError import io.element.android.tests.testutils.simulateLongTask import kotlinx.coroutines.flow.Flow @@ -20,13 +21,14 @@ import kotlinx.coroutines.flow.StateFlow class FakeSessionVerificationService( initialSessionVerifiedStatus: SessionVerifiedStatus = SessionVerifiedStatus.Unknown, - private val requestVerificationLambda: () -> Unit = { lambdaError() }, + private val requestCurrentSessionVerificationLambda: () -> Unit = { lambdaError() }, + private val requestUserVerificationLambda: (UserId) -> Unit = { lambdaError() }, private val cancelVerificationLambda: () -> Unit = { lambdaError() }, private val approveVerificationLambda: () -> Unit = { lambdaError() }, private val declineVerificationLambda: () -> Unit = { lambdaError() }, private val startVerificationLambda: () -> Unit = { lambdaError() }, private val resetLambda: (Boolean) -> Unit = { lambdaError() }, - private val acknowledgeVerificationRequestLambda: (SessionVerificationRequestDetails) -> Unit = { lambdaError() }, + private val acknowledgeVerificationRequestLambda: (VerificationRequest.Incoming) -> Unit = { lambdaError() }, private val acceptVerificationRequestLambda: () -> Unit = { lambdaError() }, ) : SessionVerificationService { private val _sessionVerifiedStatus = MutableStateFlow(initialSessionVerifiedStatus) @@ -37,8 +39,12 @@ class FakeSessionVerificationService( override val sessionVerifiedStatus: StateFlow = _sessionVerifiedStatus override val needsSessionVerification: Flow = _needsSessionVerification - override suspend fun requestVerification() { - requestVerificationLambda() + override suspend fun requestCurrentSessionVerification() { + requestCurrentSessionVerificationLambda() + } + + override suspend fun requestUserVerification(userId: UserId) { + requestUserVerificationLambda(userId) } override suspend fun cancelVerification() { @@ -68,8 +74,8 @@ class FakeSessionVerificationService( this.listener = listener } - override suspend fun acknowledgeVerificationRequest(details: SessionVerificationRequestDetails) { - acknowledgeVerificationRequestLambda(details) + override suspend fun acknowledgeVerificationRequest(verificationRequest: VerificationRequest.Incoming) { + acknowledgeVerificationRequestLambda(verificationRequest) } override suspend fun acceptVerificationRequest() = simulateLongTask { diff --git a/libraries/ui-strings/build.gradle.kts b/libraries/ui-strings/build.gradle.kts index 2f6a51497d..7e34891c66 100644 --- a/libraries/ui-strings/build.gradle.kts +++ b/libraries/ui-strings/build.gradle.kts @@ -11,4 +11,8 @@ plugins { android { namespace = "io.element.android.libraries.ui.strings" + + lint { + disable += "Typos" + } } diff --git a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/EnsureNeverCalled.kt b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/EnsureNeverCalled.kt index 689a52c792..fe45364f49 100644 --- a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/EnsureNeverCalled.kt +++ b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/EnsureNeverCalled.kt @@ -38,3 +38,9 @@ class EnsureNeverCalledWithTwoParamsAndResult : (T, U) -> R { lambdaError("Should not be called and is called with $p1 and $p2") } } + +class EnsureNeverCalledWithThreeParams : (T, U, V) -> Unit { + override fun invoke(p1: T, p2: U, p3: V) { + lambdaError("Should not be called and is called with $p1, $p2 and $p3") + } +} diff --git a/tests/uitests/src/test/snapshots/images/features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_0_en.png new file mode 100644 index 0000000000..2dd4cdac47 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:470cb7c851af9d91cc44d52d502b50764136b079b600ca58ae2512e7ff12e34b +size 29534 diff --git a/tests/uitests/src/test/snapshots/images/features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_1_en.png new file mode 100644 index 0000000000..08cde67f54 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:49db4684bd8f16361848130650ce1ee0fbc49fff5452fab177e5920ab857c1a9 +size 24663 diff --git a/tests/uitests/src/test/snapshots/images/features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_2_en.png new file mode 100644 index 0000000000..7e53aff661 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_2_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d1504a5137b90bf6c03ae8d3a90946ae1a9b528013f9c3898a0e91e4d073d159 +size 34347 diff --git a/tests/uitests/src/test/snapshots/images/features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_3_en.png new file mode 100644 index 0000000000..6736d8bf0a --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bd973e0f9c5940e3f216f27af682fffd156535b6d759e4cec01c3979eb35f1c9 +size 29624 diff --git a/tests/uitests/src/test/snapshots/images/features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Night_0_en.png new file mode 100644 index 0000000000..c6692886f4 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9dba1ef09211361216ee035c64ee50d62ab1df5191d365ef26f5a4f741477867 +size 28635 diff --git a/tests/uitests/src/test/snapshots/images/features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Night_1_en.png new file mode 100644 index 0000000000..cdb70c0358 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Night_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ba7502251d19d1fe54fa631b7c71afcdfc3c864cbab04d741a0c4a8d691b2e7f +size 24048 diff --git a/tests/uitests/src/test/snapshots/images/features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Night_2_en.png new file mode 100644 index 0000000000..7a11690369 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Night_2_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:14770fdc3e68b250b5f7dad6bb6fb1bf387c66774287276ab4b505c144007b33 +size 33299 diff --git a/tests/uitests/src/test/snapshots/images/features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Night_3_en.png new file mode 100644 index 0000000000..86c1142a7a --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Night_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0466b1f78eda1575072f9be443fcb5741ff9952fd215b9aa9b2281de7109da70 +size 28783 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_5_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_5_en.png index 479c21d680..726667ed5b 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7479bd11493b82e7027599a938979fd50fb75c31cf7c505cf8669db9387fb124 -size 38805 +oid sha256:f1a69af9c184578a355345037ec34016c4b4ab26a2719bfaffafa1027365a678 +size 38453 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_6_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_6_en.png index e3715da6da..62f03c88e1 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:56418af183e3287631cead30c1077d5734ff59f014c03e2ea0df8d91d38ade01 -size 42255 +oid sha256:c41fdaa2341788494026277683590e0fe58fcc6b4c93919c77dcf29fd1e95c15 +size 41752 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_5_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_5_en.png index 8953624294..a51874b64b 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:56c21416493a55bb9f105a9908f2b9e07441ec5f5ae3afb68f59fb8329cdb242 -size 39527 +oid sha256:e99e06f6281be006fb12e0a56e7523c8144256562464474cb2aa918f6576d636 +size 39091 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_6_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_6_en.png index 7396ea70f9..06d2ff4d32 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:128bc1d07c659dfdb25a5cac2513ef3dfcad6a156fd3a38457eec40a735e74c9 -size 43265 +oid sha256:d59deaee5d608c279670104c0f3bfcb5bbda90ac9779111441053ea1b9d40d4c +size 42658 diff --git a/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Day_0_en.png index 1296c86341..7653b90703 100644 --- a/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f41090fceca5845f88dbbc6b983e6330ecd3b86d2056ba4584f1fca26305bb0a -size 28943 +oid sha256:4e395d9ca4fc6b2eb1ea9a8fd6e2ad34e7397d9f64cf39e05eed50a1bfffabd7 +size 24186 diff --git a/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Day_1_en.png index bceae72d32..9f82a024d9 100644 --- a/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0516c0355081b7b2d745ec46fd160fd566be224ef7d350849c9990a8c6320563 -size 27018 +oid sha256:87cb4255c8346796a226cdfc2afb67641a3b27c2c1a15629a07c3293fcad5131 +size 22078 diff --git a/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Day_3_en.png index 630ea68a0b..c07abc7b58 100644 --- a/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f20c32a85bf93f25c51c18b79adc0f8237173dc9fe0705570634adbcc94b1350 -size 36484 +oid sha256:ba80787576dd893f90fcdc83021e47abbde4ae2bcaaca2c167178ba7c2be3886 +size 36481 diff --git a/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Day_4_en.png index 623baa9c2f..c85a2d454c 100644 --- a/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Day_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Day_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5cc7bdca7fe74fe0b16ab23eb4866b3af5245ed762c74a1edba087070cfe32b8 -size 30448 +oid sha256:3a2278fd516dff3a5c86702f09d5621e8b3bb3de1df702b5036fa114975e4d85 +size 27844 diff --git a/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Day_6_en.png b/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Day_6_en.png index ab96c44623..ca9fcec377 100644 --- a/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Day_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Day_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dfc7c5d99c0d1e5e0194a5961aac43efea94ae100fca0249f6b00e20b47cbb09 -size 26565 +oid sha256:a465e6254ae595d62a335914414ad2f33b76736f529d1e188f3d0e0d57268f65 +size 23015 diff --git a/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Day_7_en.png b/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Day_7_en.png index 7ed764b198..3c27d5ab57 100644 --- a/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Day_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Day_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e2f9d02116554413601c8b11fb75581f30ebb6ce5b8ade2a5a62a0aa4ce04705 -size 29914 +oid sha256:ef88b209eb93fb6b4e5aa411c4cdcb9ff1aa49bd44f11c2271a4d758482b145a +size 25181 diff --git a/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Night_0_en.png index 96061cad5e..572f9e52e5 100644 --- a/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f86dd19d8056f16ae1a5337c49f35d420f2b83baafb29360c8c07c3347d5a93f -size 28074 +oid sha256:6894ddb5662e7da82e5ee42fb0c9eb30afd87e15e415ba42be218ebab434d53a +size 23531 diff --git a/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Night_1_en.png index 50419932b1..1acafc94c6 100644 --- a/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:46d3c77fff43081041fb952fe9c8ea733c5ec164ca9aca548924d3cbefb52369 -size 26131 +oid sha256:0f393f620cc11b7aec7bf52f7efc5a5872ed07a14e7fbf2149b16171dbb2b03e +size 21463 diff --git a/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Night_3_en.png index db4d01fbfb..4d0032fc29 100644 --- a/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7fdbd16b4e78f890f60668fccfbe7af6c6a4f5ba22af26e9f3735dafb13e9d8f -size 33700 +oid sha256:ce136a49edadeaadc229ec485b27bf2d4ae20601e948bca46c632a4035b90130 +size 33697 diff --git a/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Night_4_en.png index 28ea8e7526..22aeb8dc7a 100644 --- a/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Night_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Night_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:79f2c2bb7772dd91b37351e4d1e658155f341d83cb6d496eded65b0b543fa2b1 -size 27898 +oid sha256:580fc1fe77975e83cf83fe582c2f3f4aa7003cf302e7dadcd6bb97c04cbbf148 +size 25517 diff --git a/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Night_6_en.png b/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Night_6_en.png index 2e9d6040cb..4351039557 100644 --- a/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Night_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Night_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4acb962472308c94998b3bc67d71a6a69b9bad0294461df374f7e74dcf703328 -size 24379 +oid sha256:4b48c3e2c5e06a612473e252ba52eead6efd741b7ea21fdb778ec9ed5952a2a2 +size 21181 diff --git a/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Night_7_en.png b/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Night_7_en.png index 14bbcdb78f..2c7ce358ee 100644 --- a/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Night_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Night_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a409716052c9c12c963668098943b8f286e41d6a10f5639d11b7b464cb9d0c3d -size 28909 +oid sha256:201c3f2385059321f7500ca8d559bb973a51aeb56fd417e943be496ae55d0869 +size 24370 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_10_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_10_en.png new file mode 100644 index 0000000000..53b828b23e --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_10_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b8634747fdeca8b754396dd32e02c36956017860118dbc33098aaa10fee358ec +size 28876 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_11_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_11_en.png new file mode 100644 index 0000000000..88d6b6c435 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_11_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:66af8c7679f62fede7b09d6661f0b78d7264a2a415cdf5faf0928aa115adfc2b +size 24514 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_12_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_12_en.png new file mode 100644 index 0000000000..03a91ddf87 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_12_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7c89798843bd3bc2a02094e200ddfc4fcdad8e71554d64c3f0cf2bfc75e894dc +size 24514 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_13_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_13_en.png new file mode 100644 index 0000000000..03a91ddf87 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_13_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7c89798843bd3bc2a02094e200ddfc4fcdad8e71554d64c3f0cf2bfc75e894dc +size 24514 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 47148f4301..6e3fdb2bd3 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:2473606524b856b4afd81e4a32b29c7456788746078387fff280f3c8eb577396 -size 41622 +oid sha256:1da0edef645c5abc391f0425e3a1f8719c1f061f66517b131a81cdd59d2bd04a +size 40105 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_2_en.png index c12e74814b..6cd8aaacec 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6a8c359687b3ec5f038602b9022ef3967af12c6632a59c56bd10fecf7d92233c -size 46457 +oid sha256:8ebb02ae3454f3cbb28e3aac886d088888251d25ba2c5e8fb7adde700f10383e +size 36323 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 fbae9acd3c..d7df699feb 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:b6576c0b77984df1930f0115e1ea90e6720f0fb5f500e186c591f0fd2eb771b6 -size 44729 +oid sha256:682472baf2a1bc5f99cfadc3177615167f1f7531923ec34374f59e263b50d79f +size 37049 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_4_en.png index 683805307f..7c87a4d514 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:90c8e131e639b6638a75468d41a3ff0107822e3450c6edbcc4d9eccdd180f6ae -size 31537 +oid sha256:f8aa70b0d5eace900fc8c58674d2f3cdcf409335a33e8cf3eea5f60fb495d009 +size 32299 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_5_en.png index 28a80f269d..93ebfdce9d 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3fc2ecdf5b8880b67e9ee9eacba9636819396236325f6ce28f0bf5755fa5ae5e -size 21892 +oid sha256:c28294b67c1d8f448300b35a4ec6ebecd105334375af61b61612b82b5509d417 +size 46402 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_6_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_6_en.png index 376d2f7449..231e55a989 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8371da870ec795fbced08edd199c6e6c53b1557b648686fe89c278def41e425b -size 24134 +oid sha256:becc11517522d030f4539c67d7b4728661135a43b05e9c902481b79539155912 +size 46738 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_7_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_7_en.png index 376d2f7449..0d8f547d67 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8371da870ec795fbced08edd199c6e6c53b1557b648686fe89c278def41e425b -size 24134 +oid sha256:baa21d8011741ac2bca82ac26fa289d016a3ddcc16d408f92812adc662e27869 +size 40088 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_8_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_8_en.png new file mode 100644 index 0000000000..c377b2577b --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_8_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c93ea8f1be90fc1bc43bc94c86eb7b7605872fbd96ac142ba1d633e20978d7a1 +size 40427 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_9_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_9_en.png new file mode 100644 index 0000000000..c4fff6eb04 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_9_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:acd3515c98dc74f265fbd4c8b82c60a65ae7c764994016ab39bf25062d70140f +size 31496 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_10_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_10_en.png new file mode 100644 index 0000000000..8a110274ca --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_10_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:227c4049433b734d591eeea0cb5cca65ce27b3f4fe385eb224ec322148865f6b +size 28321 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_11_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_11_en.png new file mode 100644 index 0000000000..f86af6507d --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_11_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a0393e0549647a0fe6ebef2372d9e6885db7225d137e44d4057e3fe0a15b1aa8 +size 23895 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_12_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_12_en.png new file mode 100644 index 0000000000..8ecf2388f5 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_12_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bb51fe5a51969f768609a5e5eaf5baa95005b094bf1995c3b304f637bf58fbc7 +size 24251 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_13_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_13_en.png new file mode 100644 index 0000000000..8ecf2388f5 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_13_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bb51fe5a51969f768609a5e5eaf5baa95005b094bf1995c3b304f637bf58fbc7 +size 24251 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 d9bfdf1991..848a72bbd5 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:6948418922ae7fc3fe1ec3475635cb8c53b15fa49128897e24b39f9b2dfc02f0 -size 40413 +oid sha256:4087f776842690767225ab62c5c1d7717cf039f4ec53494f834cbed9d1cc21c5 +size 38971 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_2_en.png index 924aa9fc28..8d074c872f 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f385489dab05d85ad04be5324e2b6d0ca4333f46671aee69ac11a9feb04f966b -size 45526 +oid sha256:31d4e4cbf57e53a5bf5e63daf0b1926d39dc67d2859a436fe1eb548830897ee3 +size 35532 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 fc0ad477da..142cf5e2fc 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:e7792bff8e012d42e7dea995152f5c0674f1c4a24e333cbfcb52d4ad4fc58ec2 -size 43783 +oid sha256:67f80eb2f896018c000419a2d57f3eb198951dafb60fa5e6e57dadd70170e711 +size 36091 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_4_en.png index c34fdec657..f2049210ad 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:744f66b5f395a854dce1f9abc8dbb43fbbee5de1d18b6c18d82a614eb2d32096 -size 30762 +oid sha256:6b753111cd6ca0f030222d97ebbbeb5deb9f3b9d64d82dd8970116a3913be06f +size 31615 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_5_en.png index e359daa223..d1de444683 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a3a1ef5cbd6ec289fe3b9a48811b77b37f71e53fa2be4e34d16a27fae8189fba -size 21226 +oid sha256:aabe3ce5f01f2be5a7a8ae7907a3d7b76b3b11a3012df4e69149084cf0fd0341 +size 45434 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_6_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_6_en.png index a585374439..4d68f63f9e 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:23dff654bad158f522929fdb6fb6911f4c511eb688bb28995f2d2a56bd747758 -size 23930 +oid sha256:3470688050248c93b696e95561bdf4be61dd57046d66eec297086c7cc449c97d +size 45801 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_7_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_7_en.png index a585374439..a967a634d1 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:23dff654bad158f522929fdb6fb6911f4c511eb688bb28995f2d2a56bd747758 -size 23930 +oid sha256:827bb9533946b491d3cc28d7026866f699aa6287a23daa0aa8869cad91e20a6d +size 39537 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_8_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_8_en.png new file mode 100644 index 0000000000..4bb302114b --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_8_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f75852d849d47db25fd64cbd71e6fcb98141a52fa35c329fb6a9ba35dd2b5eee +size 39902 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_9_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_9_en.png new file mode 100644 index 0000000000..bb9b28459c --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_9_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4d6df8d6e08ff4c4822fb75063dc9c04aa09334050ee3dc0801bcd10b7b95e7b +size 30690 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_0_en.png index 7e53aff661..66b44846eb 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d1504a5137b90bf6c03ae8d3a90946ae1a9b528013f9c3898a0e91e4d073d159 -size 34347 +oid sha256:bf4643bb5ac46e4e13725c6f333b23a9c5a8d3c54d8f2eecfae9811b7237d87c +size 31303 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_10_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_10_en.png index 6d93a90b6c..7f95a4dd26 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_10_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_10_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a0aba8429b1c853012d17fab62350edab5c91721560978b9024ed436d99f7ebe -size 33694 +oid sha256:c6f9f5dceb1e65cc382ef9a2f52f771635d390779f45304b7a7caeeb9e30d51c +size 27879 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_11_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_11_en.png index 1b3302c89c..6e507eb825 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_11_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_11_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:615107c5e6d654779b1a8fb0ac3e5511f03ac0eac2a5c3ae0424972b92401ca2 -size 5244 +oid sha256:bb1651647db82174f84e0e166f234e9e397df90fff77dd5f254fe1a18ed23d4e +size 25226 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_13_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_13_en.png index 55f09c6d9d..3212a9557b 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_13_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_13_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:54904239d5e4789b82299ec5fe2d68386a7a77aea9045bde2956c648e68bcb8a -size 30965 +oid sha256:87343c744ceb706cc5ab7d1391f914b0020a21e5e12cb3d8d6af536c1f0f71f4 +size 4017 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_1_en.png index cd629687fb..98a0ff76e4 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:67f61b340f5ebfbec1147f7ef37f31054cd37b32abf88058bdb18970bbf7d3fa -size 30082 +oid sha256:ebc36a702cc711e490f33912735a90f5cd85b1702c352931cd2f91d6598252bb +size 33166 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_2_en.png index c12e74814b..bf8a3e3927 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6a8c359687b3ec5f038602b9022ef3967af12c6632a59c56bd10fecf7d92233c -size 46457 +oid sha256:1d9f27a4db5f7ddc46d1564b16c7efba385ba1c4015473639b34d65875e7f37d +size 23186 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_3_en.png index fbae9acd3c..16a8ed358f 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b6576c0b77984df1930f0115e1ea90e6720f0fb5f500e186c591f0fd2eb771b6 -size 44729 +oid sha256:266f19ba9cc406a67db3ce12b7cdac15452ac8f0945ceccf8e7b9b247a50d227 +size 20367 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_4_en.png index af703a33cf..fc4d46ae29 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4bed9db1d0c920e48dd564d76494e09876c948b2fbdadccb8c4d23b49d28d3d9 -size 24255 +oid sha256:68bd384868363a724f8ae69ac8fc978677e8391724fdc9b5e2d11cd9b6cb93d1 +size 46825 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_5_en.png index 9afd661315..1e08933aa0 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f5b1f18748888c5f20d663f3c6f21fe577915f0b55e7e963be3ccf7e88cda07a -size 19250 +oid sha256:8e203ae7deccba3ba29f4f659d867f14aad8e515976fe2379dc20972e5f42ae5 +size 47164 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_6_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_6_en.png index 683805307f..0d8f547d67 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:90c8e131e639b6638a75468d41a3ff0107822e3450c6edbcc4d9eccdd180f6ae -size 31537 +oid sha256:baa21d8011741ac2bca82ac26fa289d016a3ddcc16d408f92812adc662e27869 +size 40088 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_7_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_7_en.png index 7e53aff661..8cb2aed49e 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d1504a5137b90bf6c03ae8d3a90946ae1a9b528013f9c3898a0e91e4d073d159 -size 34347 +oid sha256:c7adcf41fe721cf1bdff61850881590bdcb32cff258343c24eb4e44df7dab734 +size 24621 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_8_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_8_en.png index 2dd4cdac47..bc8a1643bd 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_8_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:470cb7c851af9d91cc44d52d502b50764136b079b600ca58ae2512e7ff12e34b -size 29534 +oid sha256:ad5caabfdf57933e5d0c0a9071464bb92729b4aaa6150b2117ce46542dea7fc6 +size 19655 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_9_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_9_en.png index 878c05c7f4..085b7d20e5 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_9_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_9_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b07e8c136c56d3e23fe3180e791b414c249865e0d7fa5132393bbf7aa09a72bf -size 26631 +oid sha256:37a07b591a038d78626986edfeb142b0581f028ece4742885d4c09a288497b22 +size 31904 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_0_en.png index 7a11690369..a01d276dd7 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:14770fdc3e68b250b5f7dad6bb6fb1bf387c66774287276ab4b505c144007b33 -size 33299 +oid sha256:4fe86ca7a863bb66a6abe6febcc1ee4eec188d57a18f76dd973f3dad00911691 +size 30570 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_10_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_10_en.png index 71b604907c..7674d03002 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_10_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_10_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c428e8289390624b3e4f606404ac54acc19cbe0a6df666661f309b7cc0a88031 -size 32082 +oid sha256:68fffe7c273430425bcf00ed18174c47023e114d9df8108f0681c61cb88ee6fc +size 27229 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_11_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_11_en.png index cba2bf39b2..4e0f525fec 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_11_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_11_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a360e21538876df4d8aa1b4a3e95e4982df6307a69df4d887416cbbd76b8cd99 -size 5250 +oid sha256:fd887339bef515c0403052338579d5885d6f0f2c31aa3ce511521a97841f3dc4 +size 24531 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_13_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_13_en.png index 7360143942..850faebafa 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_13_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_13_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1011caa6960fb64acf48f5d6c2933c9826f66afcdb80280d8509123fb507ab53 -size 30283 +oid sha256:c6ec71c6d10cfffbffb89364967f721762040da16b129da701eccb5568aa414e +size 3999 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_1_en.png index 43a7fe5af4..122bfaee2f 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ce120b819be555167e5c6200e068bca8c1f3a1245b5a6c7939fb012c793a4a57 -size 29220 +oid sha256:3129b8b649e5fb26cf559cce754a84788c186acee1e201989a3f405a8a4c71ac +size 32354 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_2_en.png index 924aa9fc28..5e7f4fa0b3 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f385489dab05d85ad04be5324e2b6d0ca4333f46671aee69ac11a9feb04f966b -size 45526 +oid sha256:90a6b06c59eb121a265f33299a97360e3a81c29daafc850c65f658cd4ffde99a +size 22695 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_3_en.png index fc0ad477da..a839708111 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e7792bff8e012d42e7dea995152f5c0674f1c4a24e333cbfcb52d4ad4fc58ec2 -size 43783 +oid sha256:84f577862d06ccb4f5a4c3ae9d95574dfc884f52ba08e121708c6833b417d130 +size 20033 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_4_en.png index 5883152c3e..de964a62be 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2542e7ad670ba57f203c3c07d9f6dcd654100fd7c7681846fdf3657436f261f4 -size 23951 +oid sha256:08c61fe78ffd95d42b9210613814ef5d975c97be277a025a26ddb903b4d10a8d +size 45829 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_5_en.png index f3ea2ba2cc..d96167c1b5 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:41fa05234a46873d525a7414191e110e157d8226dc5e034bf9ddbbbbd33cde37 -size 18696 +oid sha256:1954fc7c779cf4e7c15238a79a0f937965dfd074394be7642ea469bf5fd9482c +size 46196 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_6_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_6_en.png index c34fdec657..a967a634d1 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:744f66b5f395a854dce1f9abc8dbb43fbbee5de1d18b6c18d82a614eb2d32096 -size 30762 +oid sha256:827bb9533946b491d3cc28d7026866f699aa6287a23daa0aa8869cad91e20a6d +size 39537 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_7_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_7_en.png index 7a11690369..25f198a9e5 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:14770fdc3e68b250b5f7dad6bb6fb1bf387c66774287276ab4b505c144007b33 -size 33299 +oid sha256:b41f5b0620537d0e31e5bad7a6e763fc3efeb0cd75bf64ef3d283b14b5d5ba49 +size 24271 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_8_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_8_en.png index c6692886f4..950ce671d1 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_8_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9dba1ef09211361216ee035c64ee50d62ab1df5191d365ef26f5a4f741477867 -size 28635 +oid sha256:ee7867cdd6be342a0a2e48672c5b4d7d6273bdfe75d92ec5b5d110c31b930113 +size 19010 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_9_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_9_en.png index 4c3a6af338..1b47eec015 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_9_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_9_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0601e1c9d553113eb16d89e888e23320585ea139a6653d2b56e830834ae4fe68 -size 26011 +oid sha256:ddc9b2c7654db142caff95bcdfb5b3fa7d0ad16ec7025989711118fc29a25567 +size 31047 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.ui_VerificationUserProfileContent_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.ui_VerificationUserProfileContent_Day_0_en.png new file mode 100644 index 0000000000..38e1253d41 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.ui_VerificationUserProfileContent_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e5d78f00d33cad5e00719eec49f6b55959b5c23b3842a0f2abc9280961ebb29d +size 12660 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.ui_VerificationUserProfileContent_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.ui_VerificationUserProfileContent_Night_0_en.png new file mode 100644 index 0000000000..486dae34af --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.ui_VerificationUserProfileContent_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4612c8404b671a01129d2334a7aa296218bb4b00820265ef5d00fd9603e7f850 +size 12272 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_90_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_90_en.png new file mode 100644 index 0000000000..79872e5244 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_90_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3667c472d9832e0d25996889ec0887685dfac049c043a6c8a448789a5f7c6909 +size 17200 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_91_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_91_en.png new file mode 100644 index 0000000000..b630ea26fc --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_91_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0f96f824d47ed90dd4fee4cd64b807bc49dd273510d6cd2072b2bf5cdaf740dd +size 15943 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_92_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_92_en.png new file mode 100644 index 0000000000..a4fd665ea1 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_92_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:42488ef04ffbbe625b7da963a2b1c3c9cabb0d80bb77a2d4f8b4c4afdcd9bf95 +size 20337 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_BigIcon_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_BigIcon_Day_0_en.png index af2614df6b..9635585479 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_BigIcon_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_BigIcon_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6ec29adc030f96aae3952c785b71b2e712d9f4a6cc6cced5f4ac139f5ba4f3a7 -size 12393 +oid sha256:1fed22f221c1087ffb066b950918924cfc4af1618bb7724c57fa27d4cfe50340 +size 14089 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_BigIcon_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_BigIcon_Night_0_en.png index 82c063087b..08a56eb13c 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_BigIcon_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_BigIcon_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4aa2203ef22231bf8368849fcf1b17feb08292e116e5e33ed67c3f29ff138d25 -size 12535 +oid sha256:b028b36b64fecede982569046613903a8df32e7f95243a0eb4c25e89205c141f +size 14199 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_PageTitleWithIconFull_Day_6_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_PageTitleWithIconFull_Day_6_en.png new file mode 100644 index 0000000000..0684f2bcaf --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_PageTitleWithIconFull_Day_6_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d81aade2945be977896038addacdb9246d17bb1a57fb3bd0d43169e382ce9f03 +size 13126 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_PageTitleWithIconFull_Night_6_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_PageTitleWithIconFull_Night_6_en.png new file mode 100644 index 0000000000..7e7a23ed38 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_PageTitleWithIconFull_Night_6_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:610e68016011b14db56217ebcd27a86ab597c1668a3a90b09c3e39ac308f8764 +size 13054 diff --git a/tools/localazy/config.json b/tools/localazy/config.json index f99291da35..2495301fb8 100644 --- a/tools/localazy/config.json +++ b/tools/localazy/config.json @@ -233,7 +233,9 @@ "name" : ":features:ftue:impl", "includeRegex" : [ "screen_welcome_.*", - "screen_notification_optin_.*" + "screen_notification_optin_.*", + "screen_identity_.*", + "screen_session_verification_enter_recovery_key" ] }, {