Implement user verification (#4294)
* Add support for starting verification of a user * Add support for replying to incoming user verification requests * Add reset recovery key button and previews to `ChooseSelfVerificationModeView` * Add 'Profile' item in room details screen * Update screenshots * Remove `showDeviceVerifiedScreen` parameter from `NavTarget.UseAnotherDevice` * Allow exiting the FTUE flow, which will close the app. The previous state will be restored when the app is reopened. * When outgoing verification fails, move to the `Canceled` state. Then, when resetting the state machine state also reset the verification service. --------- Co-authored-by: ElementBot <android@element.io>
This commit is contained in:
parent
2ce1b17dae
commit
f73c0e42a4
145 changed files with 1662 additions and 830 deletions
|
|
@ -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<PlaceholderNode>(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<NavTarget : Any> : BaseBackPressHandlerStrategy<NavTarget, BackStack.State>() {
|
||||
override val canHandleBackPressFlow: StateFlow<Boolean> = MutableStateFlow(true)
|
||||
|
||||
override fun onBackPressed() {
|
||||
// No-op
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<ChooseSelfVerificationModeState>
|
||||
}
|
||||
|
|
@ -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<FtueSessionVerificationFlowNode.NavTarget>(
|
||||
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<ChooseSelfVerificationModeNode>(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<Callback>().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<String?>(null)
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
BackstackView()
|
||||
|
||||
OpenUrlInTabView(learnMoreUrl)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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<Plugin>,
|
||||
private val presenter: Presenter<ChooseSelfVerificationModeState>,
|
||||
private val directLogoutView: DirectLogoutView,
|
||||
) : Node(buildContext, plugins = plugins) {
|
||||
interface Callback : Plugin {
|
||||
fun onUseAnotherDevice()
|
||||
fun onUseRecoveryKey()
|
||||
fun onResetKey()
|
||||
fun onLearnMoreAboutEncryption()
|
||||
}
|
||||
|
||||
private val callback = plugins<Callback>().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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<DirectLogoutState>,
|
||||
) : Presenter<ChooseSelfVerificationModeState> {
|
||||
@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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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<ChooseSelfVerificationModeState> {
|
||||
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 = {},
|
||||
)
|
||||
|
|
@ -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 = {},
|
||||
)
|
||||
}
|
||||
|
|
@ -1,7 +1,18 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_identity_confirmation_cannot_confirm">"Can\'t confirm?"</string>
|
||||
<string name="screen_identity_confirmation_create_new_recovery_key">"Create a new recovery key"</string>
|
||||
<string name="screen_identity_confirmation_subtitle">"Verify this device to set up secure messaging."</string>
|
||||
<string name="screen_identity_confirmation_title">"Confirm your identity"</string>
|
||||
<string name="screen_identity_confirmation_use_another_device">"Use another device"</string>
|
||||
<string name="screen_identity_confirmation_use_recovery_key">"Use recovery key"</string>
|
||||
<string name="screen_identity_confirmed_subtitle">"Now you can read or send messages securely, and anyone you chat with can also trust this device."</string>
|
||||
<string name="screen_identity_confirmed_title">"Device verified"</string>
|
||||
<string name="screen_identity_use_another_device">"Use another device"</string>
|
||||
<string name="screen_identity_waiting_on_other_device">"Waiting on other device…"</string>
|
||||
<string name="screen_notification_optin_subtitle">"You can change your settings later."</string>
|
||||
<string name="screen_notification_optin_title">"Allow notifications and never miss a message"</string>
|
||||
<string name="screen_session_verification_enter_recovery_key">"Enter recovery key"</string>
|
||||
<string name="screen_welcome_bullet_1">"Calls, polls, search and more will be added later this year."</string>
|
||||
<string name="screen_welcome_bullet_2">"Message history for encrypted rooms isn’t available yet."</string>
|
||||
<string name="screen_welcome_bullet_3">"We’d love to hear from you, let us know what you think via the settings page."</string>
|
||||
|
|
|
|||
|
|
@ -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<DirectLogoutEvents, Unit> {}
|
||||
val logoutPresenter = Presenter<DirectLogoutState> {
|
||||
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<DirectLogoutState> = Presenter<DirectLogoutState> { aDirectLogoutState() }
|
||||
) = ChooseSelfVerificationModePresenter(
|
||||
encryptionService = encryptionService,
|
||||
directLogoutPresenter = directLogoutPresenter,
|
||||
)
|
||||
}
|
||||
|
|
@ -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<ComponentActivity>()
|
||||
|
||||
@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 <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue