Add SessionData.needsVerification field (#2672)
* Add `SessionData.needsVerification`: - Allows us to add a skip button for debug builds. - We can have the verification state almost instantly. - It doesn't depend on network availability to know the verification state and display the UI. * Add DB migration. - Make the skip button in the verification flow skip the whole flow including the completed screen. - Save the session as verified in `RustEncryptionService.recover(recoveryKey)`. * Enforce session verification for existing users too. * Fix verification confirmed screen subtitle (typo in id, was using the wrong string) * Update screenshots --------- Co-authored-by: ElementBot <benoitm+elementbot@element.io>
This commit is contained in:
parent
8757d1b5ad
commit
c8b5458878
44 changed files with 386 additions and 123 deletions
|
|
@ -23,10 +23,14 @@ import androidx.compose.runtime.LaunchedEffect
|
|||
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 androidx.compose.runtime.setValue
|
||||
import com.freeletics.flowredux.compose.rememberStateAndDispatch
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
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
|
||||
|
|
@ -35,6 +39,7 @@ import kotlinx.coroutines.CoroutineScope
|
|||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
import io.element.android.features.verifysession.impl.VerifySelfSessionStateMachine.Event as StateMachineEvent
|
||||
import io.element.android.features.verifysession.impl.VerifySelfSessionStateMachine.State as StateMachineState
|
||||
|
|
@ -43,20 +48,28 @@ class VerifySelfSessionPresenter @Inject constructor(
|
|||
private val sessionVerificationService: SessionVerificationService,
|
||||
private val encryptionService: EncryptionService,
|
||||
private val stateMachine: VerifySelfSessionStateMachine,
|
||||
private val buildMeta: BuildMeta,
|
||||
) : Presenter<VerifySelfSessionState> {
|
||||
@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()
|
||||
}
|
||||
val recoveryState by encryptionService.recoveryStateStateFlow.collectAsState()
|
||||
val stateAndDispatch = stateMachine.rememberStateAndDispatch()
|
||||
var skipVerification by remember { mutableStateOf(false) }
|
||||
val needsVerification by sessionVerificationService.needsVerificationFlow.collectAsState()
|
||||
val verificationFlowStep by remember {
|
||||
derivedStateOf {
|
||||
stateAndDispatch.state.value.toVerificationStep(
|
||||
canEnterRecoveryKey = recoveryState == RecoveryState.INCOMPLETE
|
||||
)
|
||||
when {
|
||||
skipVerification -> VerifySelfSessionState.VerificationStep.Skipped
|
||||
needsVerification -> stateAndDispatch.state.value.toVerificationStep(
|
||||
canEnterRecoveryKey = recoveryState == RecoveryState.INCOMPLETE
|
||||
)
|
||||
else -> VerifySelfSessionState.VerificationStep.Completed
|
||||
}
|
||||
}
|
||||
}
|
||||
// Start this after observing state machine
|
||||
|
|
@ -72,10 +85,15 @@ class VerifySelfSessionPresenter @Inject constructor(
|
|||
VerifySelfSessionViewEvents.DeclineVerification -> stateAndDispatch.dispatchAction(StateMachineEvent.DeclineChallenge)
|
||||
VerifySelfSessionViewEvents.Cancel -> stateAndDispatch.dispatchAction(StateMachineEvent.Cancel)
|
||||
VerifySelfSessionViewEvents.Reset -> stateAndDispatch.dispatchAction(StateMachineEvent.Reset)
|
||||
VerifySelfSessionViewEvents.SkipVerification -> coroutineScope.launch {
|
||||
sessionVerificationService.saveVerifiedState(true)
|
||||
skipVerification = true
|
||||
}
|
||||
}
|
||||
}
|
||||
return VerifySelfSessionState(
|
||||
verificationFlowStep = verificationFlowStep,
|
||||
displaySkipButton = buildMeta.isDebuggable,
|
||||
eventSink = ::handleEvents,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import io.element.android.libraries.matrix.api.verification.SessionVerificationD
|
|||
@Immutable
|
||||
data class VerifySelfSessionState(
|
||||
val verificationFlowStep: VerificationStep,
|
||||
val displaySkipButton: Boolean,
|
||||
val eventSink: (VerifySelfSessionViewEvents) -> Unit,
|
||||
) {
|
||||
@Stable
|
||||
|
|
@ -34,5 +35,6 @@ data class VerifySelfSessionState(
|
|||
data object Ready : VerificationStep
|
||||
data class Verifying(val data: SessionVerificationData, val state: AsyncData<Unit>) : VerificationStep
|
||||
data object Completed : VerificationStep
|
||||
data object Skipped : VerificationStep
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ import io.element.android.libraries.matrix.api.verification.VerificationEmoji
|
|||
open class VerifySelfSessionStateProvider : PreviewParameterProvider<VerifySelfSessionState> {
|
||||
override val values: Sequence<VerifySelfSessionState>
|
||||
get() = sequenceOf(
|
||||
aVerifySelfSessionState(),
|
||||
aVerifySelfSessionState(displaySkipButton = true),
|
||||
aVerifySelfSessionState(
|
||||
verificationFlowStep = VerifySelfSessionState.VerificationStep.AwaitingOtherDeviceResponse
|
||||
),
|
||||
|
|
@ -46,6 +46,10 @@ open class VerifySelfSessionStateProvider : PreviewParameterProvider<VerifySelfS
|
|||
aVerifySelfSessionState(
|
||||
verificationFlowStep = VerifySelfSessionState.VerificationStep.Initial(true)
|
||||
),
|
||||
aVerifySelfSessionState(
|
||||
verificationFlowStep = VerifySelfSessionState.VerificationStep.Completed,
|
||||
displaySkipButton = true,
|
||||
),
|
||||
// Add other state here
|
||||
)
|
||||
}
|
||||
|
|
@ -64,9 +68,11 @@ private fun aDecimalsSessionVerificationData(
|
|||
|
||||
internal fun aVerifySelfSessionState(
|
||||
verificationFlowStep: VerifySelfSessionState.VerificationStep = VerifySelfSessionState.VerificationStep.Initial(false),
|
||||
displaySkipButton: Boolean = false,
|
||||
eventSink: (VerifySelfSessionViewEvents) -> Unit = {},
|
||||
) = VerifySelfSessionState(
|
||||
verificationFlowStep = verificationFlowStep,
|
||||
displaySkipButton = displaySkipButton,
|
||||
eventSink = eventSink,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -28,8 +28,12 @@ import androidx.compose.foundation.layout.height
|
|||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
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.res.painterResource
|
||||
|
|
@ -51,11 +55,13 @@ 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.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.VerificationEmoji
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import io.element.android.features.verifysession.impl.VerifySelfSessionState.VerificationStep as FlowStep
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun VerifySelfSessionView(
|
||||
state: VerifySelfSessionState,
|
||||
|
|
@ -66,6 +72,12 @@ fun VerifySelfSessionView(
|
|||
fun resetFlow() {
|
||||
state.eventSink(VerifySelfSessionViewEvents.Reset)
|
||||
}
|
||||
val updatedOnFinished by rememberUpdatedState(newValue = onFinished)
|
||||
LaunchedEffect(state.verificationFlowStep, updatedOnFinished) {
|
||||
if (state.verificationFlowStep is FlowStep.Skipped) {
|
||||
updatedOnFinished()
|
||||
}
|
||||
}
|
||||
BackHandler {
|
||||
when (state.verificationFlowStep) {
|
||||
is FlowStep.Canceled -> resetFlow()
|
||||
|
|
@ -81,6 +93,19 @@ fun VerifySelfSessionView(
|
|||
val verificationFlowStep = state.verificationFlowStep
|
||||
HeaderFooterPage(
|
||||
modifier = modifier,
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = {},
|
||||
actions = {
|
||||
if (state.displaySkipButton && state.verificationFlowStep != FlowStep.Completed) {
|
||||
TextButton(
|
||||
text = stringResource(CommonStrings.action_skip),
|
||||
onClick = { state.eventSink(VerifySelfSessionViewEvents.SkipVerification) }
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
header = {
|
||||
HeaderContent(verificationFlowStep = verificationFlowStep)
|
||||
},
|
||||
|
|
@ -104,6 +129,7 @@ private fun HeaderContent(verificationFlowStep: FlowStep) {
|
|||
FlowStep.Canceled -> BigIcon.Style.AlertSolid
|
||||
FlowStep.Ready, is FlowStep.Verifying -> BigIcon.Style.Default(CompoundIcons.Reaction())
|
||||
FlowStep.Completed -> BigIcon.Style.SuccessSolid
|
||||
is FlowStep.Skipped -> return
|
||||
}
|
||||
val titleTextId = when (verificationFlowStep) {
|
||||
is FlowStep.Initial, FlowStep.AwaitingOtherDeviceResponse -> R.string.screen_identity_confirmation_title
|
||||
|
|
@ -114,20 +140,21 @@ private fun HeaderContent(verificationFlowStep: FlowStep) {
|
|||
is SessionVerificationData.Decimals -> R.string.screen_session_verification_compare_numbers_title
|
||||
is SessionVerificationData.Emojis -> R.string.screen_session_verification_compare_emojis_title
|
||||
}
|
||||
is FlowStep.Skipped -> return
|
||||
}
|
||||
val subtitleTextId = when (verificationFlowStep) {
|
||||
is FlowStep.Initial, FlowStep.AwaitingOtherDeviceResponse -> R.string.screen_identity_confirmation_subtitle
|
||||
FlowStep.Canceled -> R.string.screen_session_verification_cancelled_subtitle
|
||||
FlowStep.Ready -> R.string.screen_session_verification_ready_subtitle
|
||||
FlowStep.Completed -> R.string.screen_identity_confirmation_subtitle
|
||||
FlowStep.Completed -> R.string.screen_identity_confirmed_subtitle
|
||||
is FlowStep.Verifying -> when (verificationFlowStep.data) {
|
||||
is SessionVerificationData.Decimals -> R.string.screen_session_verification_compare_numbers_subtitle
|
||||
is SessionVerificationData.Emojis -> R.string.screen_session_verification_compare_emojis_subtitle
|
||||
}
|
||||
is FlowStep.Skipped -> return
|
||||
}
|
||||
|
||||
PageTitle(
|
||||
modifier = Modifier.padding(top = 60.dp),
|
||||
iconStyle = iconStyle,
|
||||
title = stringResource(id = titleTextId),
|
||||
subtitle = stringResource(id = subtitleTextId)
|
||||
|
|
@ -137,9 +164,8 @@ private fun HeaderContent(verificationFlowStep: FlowStep) {
|
|||
@Composable
|
||||
private fun Content(flowState: FlowStep) {
|
||||
Column(Modifier.fillMaxHeight(), verticalArrangement = Arrangement.Center) {
|
||||
when (flowState) {
|
||||
is FlowStep.Initial, FlowStep.AwaitingOtherDeviceResponse, FlowStep.Ready, FlowStep.Canceled, FlowStep.Completed -> Unit
|
||||
is FlowStep.Verifying -> ContentVerifying(flowState)
|
||||
if (flowState is FlowStep.Verifying) {
|
||||
ContentVerifying(flowState)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -264,6 +290,7 @@ private fun BottomMenu(
|
|||
onPositiveButtonClicked = onFinished,
|
||||
)
|
||||
}
|
||||
is FlowStep.Skipped -> return
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -23,4 +23,5 @@ sealed interface VerifySelfSessionViewEvents {
|
|||
data object DeclineVerification : VerifySelfSessionViewEvents
|
||||
data object Cancel : VerifySelfSessionViewEvents
|
||||
data object Reset : VerifySelfSessionViewEvents
|
||||
data object SkipVerification : VerifySelfSessionViewEvents
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue