Make sure we know the session verification state before showing the option to verify the session. #5521

This commit is contained in:
Benoit Marty 2025-11-04 12:11:11 +01:00
parent 32b1856dbd
commit ff67c8beef
9 changed files with 228 additions and 60 deletions

View file

@ -15,7 +15,9 @@ import androidx.compose.runtime.remember
import dev.zacsweers.metro.Inject
import io.element.android.features.logout.api.direct.DirectLogoutEvents
import io.element.android.features.logout.api.direct.DirectLogoutState
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.coroutine.mapState
import io.element.android.libraries.matrix.api.encryption.EncryptionService
import io.element.android.libraries.matrix.api.encryption.RecoveryState
@ -27,8 +29,33 @@ class ChooseSelfVerificationModePresenter(
@Composable
override fun present(): ChooseSelfVerificationModeState {
val hasDevicesToVerifyAgainst by encryptionService.hasDevicesToVerifyAgainst.collectAsState()
val recoveryState by encryptionService.recoveryStateStateFlow.collectAsState()
val canEnterRecoveryKey by remember { derivedStateOf { recoveryState == RecoveryState.INCOMPLETE } }
val canEnterRecoveryKey by encryptionService.recoveryStateStateFlow
.mapState { recoveryState ->
when (recoveryState) {
RecoveryState.WAITING_FOR_SYNC,
RecoveryState.UNKNOWN -> AsyncData.Loading()
RecoveryState.INCOMPLETE -> AsyncData.Success(true)
RecoveryState.ENABLED,
RecoveryState.DISABLED -> AsyncData.Success(false)
}
}
.collectAsState()
val buttonsState by remember {
derivedStateOf {
val canUseAnotherDevice = hasDevicesToVerifyAgainst.dataOrNull()
val canEnterRecoveryKey = canEnterRecoveryKey.dataOrNull()
if (canUseAnotherDevice == null || canEnterRecoveryKey == null) {
AsyncData.Loading()
} else {
AsyncData.Success(
ChooseSelfVerificationModeState.ButtonsState(
canUseAnotherDevice = canUseAnotherDevice,
canEnterRecoveryKey = canEnterRecoveryKey,
)
)
}
}
}
val directLogoutState = directLogoutPresenter.present()
@ -39,8 +66,7 @@ class ChooseSelfVerificationModePresenter(
}
return ChooseSelfVerificationModeState(
canUseAnotherDevice = hasDevicesToVerifyAgainst,
canEnterRecoveryKey = canEnterRecoveryKey,
buttonsState = buttonsState,
directLogoutState = directLogoutState,
eventSink = ::eventHandler,
)

View file

@ -8,10 +8,15 @@
package io.element.android.features.ftue.impl.sessionverification.choosemode
import io.element.android.features.logout.api.direct.DirectLogoutState
import io.element.android.libraries.architecture.AsyncData
data class ChooseSelfVerificationModeState(
val canUseAnotherDevice: Boolean,
val canEnterRecoveryKey: Boolean,
val buttonsState: AsyncData<ButtonsState>,
val directLogoutState: DirectLogoutState,
val eventSink: (ChooseSelfVerificationModeEvent) -> Unit,
)
) {
data class ButtonsState(
val canUseAnotherDevice: Boolean,
val canEnterRecoveryKey: Boolean,
)
}

View file

@ -9,23 +9,49 @@ 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
import io.element.android.libraries.architecture.AsyncData
class ChooseSelfVerificationModeStateProvider :
PreviewParameterProvider<ChooseSelfVerificationModeState> {
override val values = sequenceOf(
aChooseSelfVerificationModeState(canUseAnotherDevice = false, canEnterRecoveryKey = true),
aChooseSelfVerificationModeState(canUseAnotherDevice = false, canEnterRecoveryKey = false),
aChooseSelfVerificationModeState(canUseAnotherDevice = true, canEnterRecoveryKey = true),
aChooseSelfVerificationModeState(canUseAnotherDevice = true, canEnterRecoveryKey = false),
aChooseSelfVerificationModeState(
buttonsState = AsyncData.Success(
aButtonsState(canUseAnotherDevice = false, canEnterRecoveryKey = true),
),
),
aChooseSelfVerificationModeState(
buttonsState = AsyncData.Success(
aButtonsState(canUseAnotherDevice = false, canEnterRecoveryKey = false),
),
),
aChooseSelfVerificationModeState(
buttonsState = AsyncData.Success(
aButtonsState(canUseAnotherDevice = true, canEnterRecoveryKey = true),
),
),
aChooseSelfVerificationModeState(
buttonsState = AsyncData.Success(
aButtonsState(canUseAnotherDevice = true, canEnterRecoveryKey = false),
),
),
aChooseSelfVerificationModeState(
buttonsState = AsyncData.Loading(),
),
)
}
fun aChooseSelfVerificationModeState(
canUseAnotherDevice: Boolean = true,
canEnterRecoveryKey: Boolean = true,
buttonsState: AsyncData<ChooseSelfVerificationModeState.ButtonsState> = AsyncData.Success(aButtonsState()),
) = ChooseSelfVerificationModeState(
canUseAnotherDevice = canUseAnotherDevice,
canEnterRecoveryKey = canEnterRecoveryKey,
buttonsState = buttonsState,
directLogoutState = aDirectLogoutState(),
eventSink = {},
)
fun aButtonsState(
canUseAnotherDevice: Boolean = true,
canEnterRecoveryKey: Boolean = true,
) = ChooseSelfVerificationModeState.ButtonsState(
canUseAnotherDevice = canUseAnotherDevice,
canEnterRecoveryKey = canEnterRecoveryKey,
)

View file

@ -23,6 +23,7 @@ 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.architecture.AsyncData
import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
@ -50,7 +51,6 @@ fun ChooseSelfVerificationModeView(
BackHandler {
activity?.finish()
}
HeaderFooterPage(
modifier = modifier,
topBar = {
@ -73,29 +73,12 @@ fun ChooseSelfVerificationModeView(
)
},
footer = {
ButtonColumnMolecule(
modifier = Modifier.padding(bottom = 16.dp)
) {
if (state.canUseAnotherDevice) {
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,
)
}
ChooseSelfVerificationModeButtons(
state = state,
onUseAnotherDevice = onUseAnotherDevice,
onUseRecoveryKey = onUseRecoveryKey,
onResetKey = onResetKey,
)
}
) {
Row(
@ -113,6 +96,53 @@ fun ChooseSelfVerificationModeView(
}
}
@Composable
private fun ChooseSelfVerificationModeButtons(
state: ChooseSelfVerificationModeState,
onUseAnotherDevice: () -> Unit,
onUseRecoveryKey: () -> Unit,
onResetKey: () -> Unit,
) {
ButtonColumnMolecule(
modifier = Modifier.padding(bottom = 16.dp)
) {
when (state.buttonsState) {
AsyncData.Uninitialized,
is AsyncData.Failure,
is AsyncData.Loading -> {
Button(
modifier = Modifier.fillMaxWidth(),
enabled = false,
showProgress = true,
text = stringResource(CommonStrings.common_loading),
onClick = {},
)
}
is AsyncData.Success -> {
if (state.buttonsState.data.canUseAnotherDevice) {
Button(
modifier = Modifier.fillMaxWidth(),
text = stringResource(R.string.screen_identity_use_another_device),
onClick = onUseAnotherDevice,
)
}
if (state.buttonsState.data.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,
)
}
}
}
}
@PreviewsDayNight
@Composable
internal fun ChooseSelfVerificationModeViewPreview(