Merge develop into feature/fga/room_list_filters
This commit is contained in:
commit
f18e8030bf
67 changed files with 847 additions and 272 deletions
|
|
@ -78,7 +78,6 @@ class ShowLocationViewTest {
|
|||
),
|
||||
onBackPressed = EnsureNeverCalled(),
|
||||
)
|
||||
val shareContentDescription = rule.activity.getString(CommonStrings.action_share)
|
||||
rule.onNodeWithTag(TestTags.floatingActionButton.value).performClick()
|
||||
eventsRecorder.assertSingle(ShowLocationEvents.TrackMyLocation(true))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,28 +24,22 @@ 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 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.bool.orTrue
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.encryption.BackupState
|
||||
import io.element.android.libraries.matrix.api.encryption.BackupUploadState
|
||||
import io.element.android.libraries.matrix.api.encryption.EncryptionService
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
class LogoutPresenter @Inject constructor(
|
||||
private val matrixClient: MatrixClient,
|
||||
private val encryptionService: EncryptionService,
|
||||
private val featureFlagService: FeatureFlagService,
|
||||
) : Presenter<LogoutState> {
|
||||
@Composable
|
||||
override fun present(): LogoutState {
|
||||
|
|
@ -54,23 +48,12 @@ class LogoutPresenter @Inject constructor(
|
|||
mutableStateOf(AsyncAction.Uninitialized)
|
||||
}
|
||||
|
||||
val secureStorageFlag by featureFlagService.isFeatureEnabledFlow(FeatureFlags.SecureStorage)
|
||||
.collectAsState(initial = null)
|
||||
|
||||
val backupUploadState: BackupUploadState by remember(secureStorageFlag) {
|
||||
when (secureStorageFlag) {
|
||||
true -> encryptionService.waitForBackupUploadSteadyState()
|
||||
false -> flowOf(BackupUploadState.Done)
|
||||
else -> emptyFlow()
|
||||
}
|
||||
val backupUploadState: BackupUploadState by remember {
|
||||
encryptionService.waitForBackupUploadSteadyState()
|
||||
}
|
||||
.collectAsState(initial = BackupUploadState.Unknown)
|
||||
|
||||
var isLastSession by remember { mutableStateOf(false) }
|
||||
LaunchedEffect(Unit) {
|
||||
isLastSession = encryptionService.isLastDevice().getOrNull() ?: false
|
||||
}
|
||||
|
||||
val isLastDevice by encryptionService.isLastDevice.collectAsState()
|
||||
val backupState by encryptionService.backupStateStateFlow.collectAsState()
|
||||
val recoveryState by encryptionService.recoveryStateStateFlow.collectAsState()
|
||||
|
||||
|
|
@ -100,7 +83,7 @@ class LogoutPresenter @Inject constructor(
|
|||
}
|
||||
|
||||
return LogoutState(
|
||||
isLastSession = isLastSession,
|
||||
isLastDevice = isLastDevice,
|
||||
backupState = backupState,
|
||||
doesBackupExistOnServer = doesBackupExistOnServerAction.value.dataOrNull().orTrue(),
|
||||
recoveryState = recoveryState,
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ import io.element.android.libraries.matrix.api.encryption.BackupUploadState
|
|||
import io.element.android.libraries.matrix.api.encryption.RecoveryState
|
||||
|
||||
data class LogoutState(
|
||||
val isLastSession: Boolean,
|
||||
val isLastDevice: Boolean,
|
||||
val backupState: BackupState,
|
||||
val doesBackupExistOnServer: Boolean,
|
||||
val recoveryState: RecoveryState,
|
||||
|
|
|
|||
|
|
@ -27,22 +27,22 @@ open class LogoutStateProvider : PreviewParameterProvider<LogoutState> {
|
|||
override val values: Sequence<LogoutState>
|
||||
get() = sequenceOf(
|
||||
aLogoutState(),
|
||||
aLogoutState(isLastSession = true),
|
||||
aLogoutState(isLastSession = false, backupUploadState = BackupUploadState.Uploading(66, 200)),
|
||||
aLogoutState(isLastSession = true, backupUploadState = BackupUploadState.Done),
|
||||
aLogoutState(isLastDevice = true),
|
||||
aLogoutState(isLastDevice = false, backupUploadState = BackupUploadState.Uploading(66, 200)),
|
||||
aLogoutState(isLastDevice = true, backupUploadState = BackupUploadState.Done),
|
||||
aLogoutState(logoutAction = AsyncAction.Confirming),
|
||||
aLogoutState(logoutAction = AsyncAction.Loading),
|
||||
aLogoutState(logoutAction = AsyncAction.Failure(Exception("Failed to logout"))),
|
||||
aLogoutState(backupUploadState = BackupUploadState.SteadyException(SteadyStateException.Connection("No network"))),
|
||||
// Last session no recovery
|
||||
aLogoutState(isLastSession = true, recoveryState = RecoveryState.DISABLED),
|
||||
aLogoutState(isLastDevice = true, recoveryState = RecoveryState.DISABLED),
|
||||
// Last session no backup
|
||||
aLogoutState(isLastSession = true, backupState = BackupState.UNKNOWN, doesBackupExistOnServer = false),
|
||||
aLogoutState(isLastDevice = true, backupState = BackupState.UNKNOWN, doesBackupExistOnServer = false),
|
||||
)
|
||||
}
|
||||
|
||||
fun aLogoutState(
|
||||
isLastSession: Boolean = false,
|
||||
isLastDevice: Boolean = false,
|
||||
backupState: BackupState = BackupState.ENABLED,
|
||||
doesBackupExistOnServer: Boolean = true,
|
||||
recoveryState: RecoveryState = RecoveryState.ENABLED,
|
||||
|
|
@ -50,7 +50,7 @@ fun aLogoutState(
|
|||
logoutAction: AsyncAction<String?> = AsyncAction.Uninitialized,
|
||||
eventSink: (LogoutEvents) -> Unit = {},
|
||||
) = LogoutState(
|
||||
isLastSession = isLastSession,
|
||||
isLastDevice = isLastDevice,
|
||||
backupState = backupState,
|
||||
doesBackupExistOnServer = doesBackupExistOnServer,
|
||||
recoveryState = recoveryState,
|
||||
|
|
|
|||
|
|
@ -97,7 +97,7 @@ fun LogoutView(
|
|||
private fun title(state: LogoutState): String {
|
||||
return when {
|
||||
state.backupUploadState.isBackingUp() -> stringResource(id = R.string.screen_signout_key_backup_ongoing_title)
|
||||
state.isLastSession -> {
|
||||
state.isLastDevice -> {
|
||||
if (state.recoveryState != RecoveryState.ENABLED) {
|
||||
stringResource(id = R.string.screen_signout_recovery_disabled_title)
|
||||
} else if (state.backupState == BackupState.UNKNOWN && state.doesBackupExistOnServer.not()) {
|
||||
|
|
@ -116,7 +116,7 @@ private fun subtitle(state: LogoutState): String? {
|
|||
(state.backupUploadState as? BackupUploadState.SteadyException)?.exception is SteadyStateException.Connection ->
|
||||
stringResource(id = R.string.screen_signout_key_backup_offline_subtitle)
|
||||
state.backupUploadState.isBackingUp() -> stringResource(id = R.string.screen_signout_key_backup_ongoing_subtitle)
|
||||
state.isLastSession -> stringResource(id = R.string.screen_signout_key_backup_disabled_subtitle)
|
||||
state.isLastDevice -> stringResource(id = R.string.screen_signout_key_backup_disabled_subtitle)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
|
@ -128,7 +128,7 @@ private fun ColumnScope.Buttons(
|
|||
onChangeRecoveryKeyClicked: () -> Unit,
|
||||
) {
|
||||
val logoutAction = state.logoutAction
|
||||
if (state.isLastSession) {
|
||||
if (state.isLastDevice) {
|
||||
OutlinedButton(
|
||||
text = stringResource(id = CommonStrings.common_settings),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
|
|
|
|||
|
|
@ -17,14 +17,12 @@
|
|||
package io.element.android.features.logout.impl.direct
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.collectAsState
|
||||
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.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.features.logout.api.direct.DirectLogoutEvents
|
||||
import io.element.android.features.logout.api.direct.DirectLogoutPresenter
|
||||
|
|
@ -33,14 +31,10 @@ import io.element.android.features.logout.impl.tools.isBackingUp
|
|||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.runCatchingUpdatingState
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.encryption.BackupUploadState
|
||||
import io.element.android.libraries.matrix.api.encryption.EncryptionService
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
|
|
@ -48,7 +42,6 @@ import javax.inject.Inject
|
|||
class DefaultDirectLogoutPresenter @Inject constructor(
|
||||
private val matrixClient: MatrixClient,
|
||||
private val encryptionService: EncryptionService,
|
||||
private val featureFlagService: FeatureFlagService,
|
||||
) : DirectLogoutPresenter {
|
||||
@Composable
|
||||
override fun present(): DirectLogoutState {
|
||||
|
|
@ -58,22 +51,12 @@ class DefaultDirectLogoutPresenter @Inject constructor(
|
|||
mutableStateOf(AsyncAction.Uninitialized)
|
||||
}
|
||||
|
||||
val secureStorageFlag by featureFlagService.isFeatureEnabledFlow(FeatureFlags.SecureStorage)
|
||||
.collectAsState(initial = null)
|
||||
|
||||
val backupUploadState: BackupUploadState by remember(secureStorageFlag) {
|
||||
when (secureStorageFlag) {
|
||||
true -> encryptionService.waitForBackupUploadSteadyState()
|
||||
false -> flowOf(BackupUploadState.Done)
|
||||
else -> emptyFlow()
|
||||
}
|
||||
val backupUploadState: BackupUploadState by remember {
|
||||
encryptionService.waitForBackupUploadSteadyState()
|
||||
}
|
||||
.collectAsState(initial = BackupUploadState.Unknown)
|
||||
|
||||
var isLastSession by remember { mutableStateOf(false) }
|
||||
LaunchedEffect(Unit) {
|
||||
isLastSession = encryptionService.isLastDevice().getOrNull() ?: false
|
||||
}
|
||||
val isLastDevice by encryptionService.isLastDevice.collectAsState()
|
||||
|
||||
fun handleEvents(event: DirectLogoutEvents) {
|
||||
when (event) {
|
||||
|
|
@ -91,7 +74,7 @@ class DefaultDirectLogoutPresenter @Inject constructor(
|
|||
}
|
||||
|
||||
return DirectLogoutState(
|
||||
canDoDirectSignOut = !isLastSession &&
|
||||
canDoDirectSignOut = !isLastDevice &&
|
||||
!backupUploadState.isBackingUp(),
|
||||
logoutAction = logoutAction.value,
|
||||
eventSink = ::handleEvents
|
||||
|
|
|
|||
|
|
@ -22,8 +22,6 @@ import app.cash.turbine.ReceiveTurbine
|
|||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.encryption.BackupState
|
||||
import io.element.android.libraries.matrix.api.encryption.BackupUploadState
|
||||
|
|
@ -50,7 +48,7 @@ class LogoutPresenterTest {
|
|||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitFirstItem()
|
||||
assertThat(initialState.isLastSession).isFalse()
|
||||
assertThat(initialState.isLastDevice).isFalse()
|
||||
assertThat(initialState.backupState).isEqualTo(BackupState.UNKNOWN)
|
||||
assertThat(initialState.doesBackupExistOnServer).isTrue()
|
||||
assertThat(initialState.recoveryState).isEqualTo(RecoveryState.UNKNOWN)
|
||||
|
|
@ -63,15 +61,15 @@ class LogoutPresenterTest {
|
|||
fun `present - initial state - last session`() = runTest {
|
||||
val presenter = createLogoutPresenter(
|
||||
encryptionService = FakeEncryptionService().apply {
|
||||
givenIsLastDevice(true)
|
||||
emitIsLastDevice(true)
|
||||
}
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
skipItems(3)
|
||||
skipItems(2)
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.isLastSession).isTrue()
|
||||
assertThat(initialState.isLastDevice).isTrue()
|
||||
assertThat(initialState.backupUploadState).isEqualTo(BackupUploadState.Unknown)
|
||||
assertThat(initialState.logoutAction).isEqualTo(AsyncAction.Uninitialized)
|
||||
}
|
||||
|
|
@ -96,10 +94,9 @@ class LogoutPresenterTest {
|
|||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.isLastSession).isFalse()
|
||||
assertThat(initialState.isLastDevice).isFalse()
|
||||
assertThat(initialState.backupUploadState).isEqualTo(BackupUploadState.Unknown)
|
||||
assertThat(initialState.logoutAction).isEqualTo(AsyncAction.Uninitialized)
|
||||
skipItems(1)
|
||||
val waitingState = awaitItem()
|
||||
assertThat(waitingState.backupUploadState).isEqualTo(BackupUploadState.Waiting)
|
||||
skipItems(1)
|
||||
|
|
@ -209,6 +206,5 @@ class LogoutPresenterTest {
|
|||
): LogoutPresenter = LogoutPresenter(
|
||||
matrixClient = matrixClient,
|
||||
encryptionService = encryptionService,
|
||||
featureFlagService = FakeFeatureFlagService(mapOf(FeatureFlags.SecureStorage.key to true)),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -153,7 +153,7 @@ class LogoutViewTest {
|
|||
rule.setContent {
|
||||
LogoutView(
|
||||
aLogoutState(
|
||||
isLastSession = true,
|
||||
isLastDevice = true,
|
||||
eventSink = eventsRecorder
|
||||
),
|
||||
onChangeRecoveryKeyClicked = callback,
|
||||
|
|
|
|||
|
|
@ -23,8 +23,6 @@ import app.cash.turbine.test
|
|||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.logout.api.direct.DirectLogoutEvents
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.encryption.BackupUploadState
|
||||
import io.element.android.libraries.matrix.api.encryption.EncryptionService
|
||||
|
|
@ -57,14 +55,13 @@ class DefaultDirectLogoutPresenterTest {
|
|||
fun `present - initial state - last session`() = runTest {
|
||||
val presenter = createDefaultDirectLogoutPresenter(
|
||||
encryptionService = FakeEncryptionService().apply {
|
||||
givenIsLastDevice(true)
|
||||
emitIsLastDevice(true)
|
||||
}
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
skipItems(2)
|
||||
val initialState = awaitItem()
|
||||
val initialState = awaitFirstItem()
|
||||
assertThat(initialState.canDoDirectSignOut).isFalse()
|
||||
assertThat(initialState.logoutAction).isEqualTo(AsyncAction.Uninitialized)
|
||||
}
|
||||
|
|
@ -84,8 +81,8 @@ class DefaultDirectLogoutPresenterTest {
|
|||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
skipItems(2)
|
||||
val initialState = awaitItem()
|
||||
skipItems(1)
|
||||
val initialState = awaitFirstItem()
|
||||
assertThat(initialState.canDoDirectSignOut).isFalse()
|
||||
assertThat(initialState.logoutAction).isEqualTo(AsyncAction.Uninitialized)
|
||||
}
|
||||
|
|
@ -180,7 +177,6 @@ class DefaultDirectLogoutPresenterTest {
|
|||
}
|
||||
|
||||
private suspend fun <T> ReceiveTurbine<T>.awaitFirstItem(): T {
|
||||
skipItems(1)
|
||||
return awaitItem()
|
||||
}
|
||||
|
||||
|
|
@ -190,6 +186,5 @@ class DefaultDirectLogoutPresenterTest {
|
|||
): DefaultDirectLogoutPresenter = DefaultDirectLogoutPresenter(
|
||||
matrixClient = matrixClient,
|
||||
encryptionService = encryptionService,
|
||||
featureFlagService = FakeFeatureFlagService(mapOf(FeatureFlags.SecureStorage.key to true)),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -79,9 +79,6 @@ class PreferencesRootPresenter @Inject constructor(
|
|||
|
||||
val showSecureBackupIndicator by indicatorService.showSettingChatBackupIndicator()
|
||||
|
||||
val secureStorageFlag by featureFlagService.isFeatureEnabledFlow(FeatureFlags.SecureStorage)
|
||||
.collectAsState(initial = null)
|
||||
|
||||
val accountManagementUrl: MutableState<String?> = remember {
|
||||
mutableStateOf(null)
|
||||
}
|
||||
|
|
@ -101,7 +98,7 @@ class PreferencesRootPresenter @Inject constructor(
|
|||
version = versionFormatter.get(),
|
||||
deviceId = matrixClient.deviceId,
|
||||
showCompleteVerification = showCompleteVerification,
|
||||
showSecureBackup = !showCompleteVerification && secureStorageFlag == true,
|
||||
showSecureBackup = !showCompleteVerification,
|
||||
showSecureBackupBadge = showSecureBackupIndicator,
|
||||
accountManagementUrl = accountManagementUrl.value,
|
||||
devicesManagementUrl = devicesManagementUrl.value,
|
||||
|
|
|
|||
|
|
@ -65,7 +65,6 @@ class PreferencesRootPresenterTest {
|
|||
indicatorService = DefaultIndicatorService(
|
||||
sessionVerificationService = sessionVerificationService,
|
||||
encryptionService = FakeEncryptionService(),
|
||||
featureFlagService = FakeFeatureFlagService(),
|
||||
),
|
||||
directLogoutPresenter = object : DirectLogoutPresenter {
|
||||
@Composable
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ interface RoomListEntryPoint : FeatureEntryPoint {
|
|||
fun onCreateRoomClicked()
|
||||
fun onSettingsClicked()
|
||||
fun onSessionVerificationClicked()
|
||||
fun onSessionConfirmRecoveryKeyClicked()
|
||||
fun onInvitesClicked()
|
||||
fun onRoomSettingsClicked(roomId: RoomId)
|
||||
fun onReportBugClicked()
|
||||
|
|
|
|||
|
|
@ -68,6 +68,10 @@ class RoomListNode @AssistedInject constructor(
|
|||
plugins<RoomListEntryPoint.Callback>().forEach { it.onSessionVerificationClicked() }
|
||||
}
|
||||
|
||||
private fun onSessionConfirmRecoveryKeyClicked() {
|
||||
plugins<RoomListEntryPoint.Callback>().forEach { it.onSessionConfirmRecoveryKeyClicked() }
|
||||
}
|
||||
|
||||
private fun onInvitesClicked() {
|
||||
plugins<RoomListEntryPoint.Callback>().forEach { it.onInvitesClicked() }
|
||||
}
|
||||
|
|
@ -97,6 +101,7 @@ class RoomListNode @AssistedInject constructor(
|
|||
onSettingsClicked = this::onOpenSettings,
|
||||
onCreateRoomClicked = this::onCreateRoomClicked,
|
||||
onVerifyClicked = this::onSessionVerificationClicked,
|
||||
onConfirmRecoveryKeyClicked = this::onSessionConfirmRecoveryKeyClicked,
|
||||
onInvitesClicked = this::onInvitesClicked,
|
||||
onRoomSettingsClicked = this::onRoomSettingsClicked,
|
||||
onMenuActionClicked = { onMenuActionClicked(activity, it) },
|
||||
|
|
|
|||
|
|
@ -52,6 +52,8 @@ import io.element.android.libraries.matrix.api.MatrixClient
|
|||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
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.sync.SyncService
|
||||
import io.element.android.libraries.matrix.api.sync.SyncState
|
||||
import io.element.android.libraries.matrix.api.timeline.ReceiptType
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.matrix.api.user.getCurrentUser
|
||||
|
|
@ -74,7 +76,6 @@ private const val EXTENDED_RANGE_SIZE = 40
|
|||
|
||||
class RoomListPresenter @Inject constructor(
|
||||
private val client: MatrixClient,
|
||||
private val sessionVerificationService: SessionVerificationService,
|
||||
private val networkMonitor: NetworkMonitor,
|
||||
private val snackbarDispatcher: SnackbarDispatcher,
|
||||
private val inviteStateDataSource: InviteStateDataSource,
|
||||
|
|
@ -89,6 +90,8 @@ class RoomListPresenter @Inject constructor(
|
|||
private val analyticsService: AnalyticsService,
|
||||
) : Presenter<RoomListState> {
|
||||
private val encryptionService: EncryptionService = client.encryptionService()
|
||||
private val sessionVerificationService: SessionVerificationService = client.sessionVerificationService()
|
||||
private val syncService: SyncService = client.syncService()
|
||||
|
||||
@Composable
|
||||
override fun present(): RoomListState {
|
||||
|
|
@ -112,22 +115,24 @@ class RoomListPresenter @Inject constructor(
|
|||
|
||||
val isMigrating = migrationScreenPresenter.present().isMigrating
|
||||
|
||||
// Session verification status (unknown, not verified, verified)
|
||||
var securityBannerDismissed by rememberSaveable { mutableStateOf(false) }
|
||||
val canVerifySession by sessionVerificationService.canVerifySessionFlow.collectAsState(initial = false)
|
||||
var verificationPromptDismissed by rememberSaveable { mutableStateOf(false) }
|
||||
// We combine both values to only display the prompt if the session is not verified and it wasn't dismissed
|
||||
val displayVerificationPrompt by remember {
|
||||
derivedStateOf { canVerifySession && !verificationPromptDismissed }
|
||||
}
|
||||
val isLastDevice by encryptionService.isLastDevice.collectAsState()
|
||||
val recoveryState by encryptionService.recoveryStateStateFlow.collectAsState()
|
||||
val secureStorageFlag by featureFlagService.isFeatureEnabledFlow(FeatureFlags.SecureStorage)
|
||||
.collectAsState(initial = null)
|
||||
var recoveryKeyPromptDismissed by rememberSaveable { mutableStateOf(false) }
|
||||
val displayRecoveryKeyPrompt by remember {
|
||||
val syncState by syncService.syncState.collectAsState()
|
||||
val securityBannerState by remember {
|
||||
derivedStateOf {
|
||||
secureStorageFlag == true &&
|
||||
when {
|
||||
securityBannerDismissed -> SecurityBannerState.None
|
||||
canVerifySession -> if (isLastDevice) {
|
||||
SecurityBannerState.RecoveryKeyConfirmation
|
||||
} else {
|
||||
SecurityBannerState.SessionVerification
|
||||
}
|
||||
recoveryState == RecoveryState.INCOMPLETE &&
|
||||
!recoveryKeyPromptDismissed
|
||||
syncState == SyncState.Running -> SecurityBannerState.RecoveryKeyConfirmation
|
||||
else -> SecurityBannerState.None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -139,8 +144,8 @@ class RoomListPresenter @Inject constructor(
|
|||
fun handleEvents(event: RoomListEvents) {
|
||||
when (event) {
|
||||
is RoomListEvents.UpdateVisibleRange -> updateVisibleRange(event.range)
|
||||
RoomListEvents.DismissRequestVerificationPrompt -> verificationPromptDismissed = true
|
||||
RoomListEvents.DismissRecoveryKeyPrompt -> recoveryKeyPromptDismissed = true
|
||||
RoomListEvents.DismissRequestVerificationPrompt -> securityBannerDismissed = true
|
||||
RoomListEvents.DismissRecoveryKeyPrompt -> securityBannerDismissed = true
|
||||
RoomListEvents.ToggleSearchResults -> searchState.eventSink(RoomListSearchEvents.ToggleSearchVisibility)
|
||||
is RoomListEvents.ShowContextMenu -> {
|
||||
coroutineScope.showContextMenu(event, contextMenu)
|
||||
|
|
@ -161,8 +166,7 @@ class RoomListPresenter @Inject constructor(
|
|||
matrixUser = matrixUser.value,
|
||||
showAvatarIndicator = showAvatarIndicator,
|
||||
roomList = roomList,
|
||||
displayVerificationPrompt = displayVerificationPrompt,
|
||||
displayRecoveryKeyPrompt = displayRecoveryKeyPrompt,
|
||||
securityBannerState = securityBannerState,
|
||||
snackbarMessage = snackbarMessage,
|
||||
hasNetworkConnection = networkConnectionStatus == NetworkStatus.Online,
|
||||
invitesState = inviteStateDataSource.inviteState(),
|
||||
|
|
|
|||
|
|
@ -32,8 +32,7 @@ data class RoomListState(
|
|||
val matrixUser: MatrixUser?,
|
||||
val showAvatarIndicator: Boolean,
|
||||
val roomList: AsyncData<ImmutableList<RoomListRoomSummary>>,
|
||||
val displayVerificationPrompt: Boolean,
|
||||
val displayRecoveryKeyPrompt: Boolean,
|
||||
val securityBannerState: SecurityBannerState,
|
||||
val hasNetworkConnection: Boolean,
|
||||
val snackbarMessage: SnackbarMessage?,
|
||||
val invitesState: InvitesState,
|
||||
|
|
@ -65,3 +64,9 @@ enum class InvitesState {
|
|||
SeenInvites,
|
||||
NewInvites,
|
||||
}
|
||||
|
||||
enum class SecurityBannerState {
|
||||
None,
|
||||
SessionVerification,
|
||||
RecoveryKeyConfirmation,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,11 +17,14 @@
|
|||
package io.element.android.features.roomlist.impl
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomState
|
||||
import io.element.android.features.leaveroom.api.aLeaveRoomState
|
||||
import io.element.android.features.roomlist.impl.datasource.RoomListRoomSummaryFactory
|
||||
import io.element.android.features.roomlist.impl.filters.RoomListFiltersState
|
||||
import io.element.android.features.roomlist.impl.filters.aRoomListFiltersState
|
||||
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
|
||||
import io.element.android.features.roomlist.impl.model.aRoomListRoomSummary
|
||||
import io.element.android.features.roomlist.impl.search.RoomListSearchState
|
||||
import io.element.android.features.roomlist.impl.search.aRoomListSearchState
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
|
|
@ -38,37 +41,50 @@ open class RoomListStateProvider : PreviewParameterProvider<RoomListState> {
|
|||
override val values: Sequence<RoomListState>
|
||||
get() = sequenceOf(
|
||||
aRoomListState(),
|
||||
aRoomListState().copy(displayVerificationPrompt = true),
|
||||
aRoomListState().copy(snackbarMessage = SnackbarMessage(CommonStrings.common_verification_complete)),
|
||||
aRoomListState().copy(hasNetworkConnection = false),
|
||||
aRoomListState().copy(invitesState = InvitesState.SeenInvites),
|
||||
aRoomListState().copy(invitesState = InvitesState.NewInvites),
|
||||
aRoomListState().copy(contextMenu = aContextMenuShown(roomName = "A nice room name")),
|
||||
aRoomListState().copy(contextMenu = aContextMenuShown(isFavorite = true)),
|
||||
aRoomListState().copy(displayRecoveryKeyPrompt = true),
|
||||
aRoomListState().copy(roomList = AsyncData.Success(persistentListOf())),
|
||||
aRoomListState().copy(roomList = AsyncData.Loading(prevData = RoomListRoomSummaryFactory.createFakeList())),
|
||||
aRoomListState().copy(matrixUser = null, displayMigrationStatus = true),
|
||||
aRoomListState().copy(searchState = aRoomListSearchState(isSearchActive = true, query = "Test")),
|
||||
aRoomListState().copy(filtersState = aRoomListFiltersState(isFeatureEnabled = true)),
|
||||
aRoomListState(snackbarMessage = SnackbarMessage(CommonStrings.common_verification_complete)),
|
||||
aRoomListState(hasNetworkConnection = false),
|
||||
aRoomListState(invitesState = InvitesState.SeenInvites),
|
||||
aRoomListState(invitesState = InvitesState.NewInvites),
|
||||
aRoomListState(contextMenu = aContextMenuShown(roomName = "A nice room name")),
|
||||
aRoomListState(contextMenu = aContextMenuShown(isFavorite = true)),
|
||||
aRoomListState(securityBannerState = SecurityBannerState.SessionVerification),
|
||||
aRoomListState(securityBannerState = SecurityBannerState.RecoveryKeyConfirmation),
|
||||
aRoomListState(roomList = AsyncData.Success(persistentListOf())),
|
||||
aRoomListState(roomList = AsyncData.Loading(prevData = RoomListRoomSummaryFactory.createFakeList())),
|
||||
aRoomListState(matrixUser = null, displayMigrationStatus = true),
|
||||
aRoomListState(searchState = aRoomListSearchState(isSearchActive = true, query = "Test")),
|
||||
aRoomListState(filtersState = aRoomListFiltersState(isFeatureEnabled = true)),
|
||||
)
|
||||
}
|
||||
|
||||
internal fun aRoomListState() = RoomListState(
|
||||
matrixUser = MatrixUser(userId = UserId("@id:domain"), displayName = "User#1"),
|
||||
showAvatarIndicator = false,
|
||||
roomList = AsyncData.Success(aRoomListRoomSummaryList()),
|
||||
hasNetworkConnection = true,
|
||||
snackbarMessage = null,
|
||||
displayVerificationPrompt = false,
|
||||
displayRecoveryKeyPrompt = false,
|
||||
invitesState = InvitesState.NoInvites,
|
||||
contextMenu = RoomListState.ContextMenu.Hidden,
|
||||
leaveRoomState = aLeaveRoomState(),
|
||||
filtersState = aRoomListFiltersState(isFeatureEnabled = false),
|
||||
searchState = aRoomListSearchState(),
|
||||
displayMigrationStatus = false,
|
||||
eventSink = {}
|
||||
internal fun aRoomListState(
|
||||
matrixUser: MatrixUser? = MatrixUser(userId = UserId("@id:domain"), displayName = "User#1"),
|
||||
showAvatarIndicator: Boolean = false,
|
||||
roomList: AsyncData<ImmutableList<RoomListRoomSummary>> = AsyncData.Success(aRoomListRoomSummaryList()),
|
||||
hasNetworkConnection: Boolean = true,
|
||||
snackbarMessage: SnackbarMessage? = null,
|
||||
securityBannerState: SecurityBannerState = SecurityBannerState.None,
|
||||
invitesState: InvitesState = InvitesState.NoInvites,
|
||||
contextMenu: RoomListState.ContextMenu = RoomListState.ContextMenu.Hidden,
|
||||
leaveRoomState: LeaveRoomState = aLeaveRoomState(),
|
||||
searchState: RoomListSearchState = aRoomListSearchState(),
|
||||
filtersState: RoomListFiltersState = aRoomListFiltersState(isFeatureEnabled = false),
|
||||
displayMigrationStatus: Boolean = false,
|
||||
eventSink: (RoomListEvents) -> Unit = {}
|
||||
) = RoomListState(
|
||||
matrixUser = matrixUser,
|
||||
showAvatarIndicator = showAvatarIndicator,
|
||||
roomList = roomList,
|
||||
hasNetworkConnection = hasNetworkConnection,
|
||||
snackbarMessage = snackbarMessage,
|
||||
securityBannerState = securityBannerState,
|
||||
invitesState = invitesState,
|
||||
contextMenu = contextMenu,
|
||||
leaveRoomState = leaveRoomState,
|
||||
searchState = searchState,
|
||||
filtersState = filtersState,
|
||||
displayMigrationStatus = displayMigrationStatus,
|
||||
eventSink = eventSink,
|
||||
)
|
||||
|
||||
internal fun aRoomListRoomSummaryList(): ImmutableList<RoomListRoomSummary> {
|
||||
|
|
|
|||
|
|
@ -79,6 +79,7 @@ fun RoomListView(
|
|||
onRoomClicked: (RoomId) -> Unit,
|
||||
onSettingsClicked: () -> Unit,
|
||||
onVerifyClicked: () -> Unit,
|
||||
onConfirmRecoveryKeyClicked: () -> Unit,
|
||||
onCreateRoomClicked: () -> Unit,
|
||||
onInvitesClicked: () -> Unit,
|
||||
onRoomSettingsClicked: (roomId: RoomId) -> Unit,
|
||||
|
|
@ -110,6 +111,7 @@ fun RoomListView(
|
|||
modifier = Modifier.padding(top = topPadding),
|
||||
state = state,
|
||||
onVerifyClicked = onVerifyClicked,
|
||||
onConfirmRecoveryKeyClicked = onConfirmRecoveryKeyClicked,
|
||||
onRoomClicked = onRoomClicked,
|
||||
onRoomLongClicked = { onRoomLongClicked(it) },
|
||||
onOpenSettings = onSettingsClicked,
|
||||
|
|
@ -167,6 +169,7 @@ private fun EmptyRoomListView(
|
|||
private fun RoomListContent(
|
||||
state: RoomListState,
|
||||
onVerifyClicked: () -> Unit,
|
||||
onConfirmRecoveryKeyClicked: () -> Unit,
|
||||
onRoomClicked: (RoomId) -> Unit,
|
||||
onRoomLongClicked: (RoomListRoomSummary) -> Unit,
|
||||
onOpenSettings: () -> Unit,
|
||||
|
|
@ -233,7 +236,7 @@ private fun RoomListContent(
|
|||
) {
|
||||
when {
|
||||
state.displayEmptyState -> Unit
|
||||
state.displayVerificationPrompt -> {
|
||||
state.securityBannerState == SecurityBannerState.SessionVerification -> {
|
||||
item {
|
||||
RequestVerificationHeader(
|
||||
onVerifyClicked = onVerifyClicked,
|
||||
|
|
@ -241,10 +244,10 @@ private fun RoomListContent(
|
|||
)
|
||||
}
|
||||
}
|
||||
state.displayRecoveryKeyPrompt -> {
|
||||
state.securityBannerState == SecurityBannerState.RecoveryKeyConfirmation -> {
|
||||
item {
|
||||
ConfirmRecoveryKeyBanner(
|
||||
onContinueClicked = onOpenSettings,
|
||||
onContinueClicked = onConfirmRecoveryKeyClicked,
|
||||
onDismissClicked = { state.eventSink(RoomListEvents.DismissRecoveryKeyPrompt) }
|
||||
)
|
||||
}
|
||||
|
|
@ -312,6 +315,7 @@ internal fun RoomListViewPreview(@PreviewParameter(RoomListStateProvider::class)
|
|||
onRoomClicked = {},
|
||||
onSettingsClicked = {},
|
||||
onVerifyClicked = {},
|
||||
onConfirmRecoveryKeyClicked = {},
|
||||
onCreateRoomClicked = {},
|
||||
onInvitesClicked = {},
|
||||
onRoomSettingsClicked = {},
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="confirm_recovery_key_banner_message">"Your chat backup is currently out of sync. You need to confirm your recovery key to maintain access to your chat backup."</string>
|
||||
<string name="confirm_recovery_key_banner_title">"Confirm your recovery key"</string>
|
||||
<string name="confirm_recovery_key_banner_message">"Your chat backup is currently out of sync. You need to enter your recovery key to maintain access to your chat backup."</string>
|
||||
<string name="confirm_recovery_key_banner_title">"Enter your recovery key"</string>
|
||||
<string name="screen_migration_message">"This is a one time process, thanks for waiting."</string>
|
||||
<string name="screen_migration_title">"Setting up your account."</string>
|
||||
<string name="screen_roomlist_a11y_create_message">"Create a new conversation or room"</string>
|
||||
|
|
|
|||
|
|
@ -47,7 +47,6 @@ import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatch
|
|||
import io.element.android.libraries.eventformatter.api.RoomLastMessageFormatter
|
||||
import io.element.android.libraries.eventformatter.test.FakeRoomLastMessageFormatter
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
||||
import io.element.android.libraries.featureflag.test.InMemorySessionPreferencesStore
|
||||
import io.element.android.libraries.indicator.impl.DefaultIndicatorService
|
||||
|
|
@ -56,9 +55,8 @@ import io.element.android.libraries.matrix.api.encryption.BackupState
|
|||
import io.element.android.libraries.matrix.api.encryption.RecoveryState
|
||||
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
|
||||
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
||||
import io.element.android.libraries.matrix.api.sync.SyncState
|
||||
import io.element.android.libraries.matrix.api.timeline.ReceiptType
|
||||
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
||||
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
|
||||
import io.element.android.libraries.matrix.test.AN_AVATAR_URL
|
||||
import io.element.android.libraries.matrix.test.AN_EXCEPTION
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||
|
|
@ -72,6 +70,7 @@ import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
|||
import io.element.android.libraries.matrix.test.room.aRoomInfo
|
||||
import io.element.android.libraries.matrix.test.room.aRoomSummaryFilled
|
||||
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
|
||||
import io.element.android.libraries.matrix.test.sync.FakeSyncService
|
||||
import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||
|
|
@ -107,7 +106,7 @@ class RoomListPresenterTests {
|
|||
assertThat(withUserState.matrixUser!!.userId).isEqualTo(A_USER_ID)
|
||||
assertThat(withUserState.matrixUser!!.displayName).isEqualTo(A_USER_NAME)
|
||||
assertThat(withUserState.matrixUser!!.avatarUrl).isEqualTo(AN_AVATAR_URL)
|
||||
assertThat(withUserState.showAvatarIndicator).isFalse()
|
||||
assertThat(withUserState.showAvatarIndicator).isTrue()
|
||||
scope.cancel()
|
||||
}
|
||||
}
|
||||
|
|
@ -116,13 +115,13 @@ class RoomListPresenterTests {
|
|||
fun `present - show avatar indicator`() = runTest {
|
||||
val scope = CoroutineScope(coroutineContext + SupervisorJob())
|
||||
val encryptionService = FakeEncryptionService()
|
||||
val sessionVerificationService = FakeSessionVerificationService()
|
||||
val matrixClient = FakeMatrixClient(
|
||||
encryptionService = encryptionService,
|
||||
sessionVerificationService = sessionVerificationService,
|
||||
)
|
||||
val sessionVerificationService = FakeSessionVerificationService()
|
||||
val presenter = createRoomListPresenter(
|
||||
client = matrixClient,
|
||||
sessionVerificationService = sessionVerificationService,
|
||||
coroutineScope = scope
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
|
|
@ -130,12 +129,12 @@ class RoomListPresenterTests {
|
|||
}.test {
|
||||
skipItems(1)
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.showAvatarIndicator).isFalse()
|
||||
assertThat(initialState.showAvatarIndicator).isTrue()
|
||||
sessionVerificationService.givenCanVerifySession(false)
|
||||
assertThat(awaitItem().showAvatarIndicator).isFalse()
|
||||
encryptionService.emitBackupState(BackupState.UNKNOWN)
|
||||
assertThat(awaitItem().showAvatarIndicator).isTrue()
|
||||
encryptionService.emitBackupState(BackupState.ENABLED)
|
||||
val finalState = awaitItem()
|
||||
assertThat(finalState.showAvatarIndicator).isTrue()
|
||||
assertThat(finalState.showAvatarIndicator).isFalse()
|
||||
scope.cancel()
|
||||
}
|
||||
}
|
||||
|
|
@ -240,28 +239,42 @@ class RoomListPresenterTests {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `present - handle DismissRequestVerificationPrompt`() = runTest {
|
||||
val roomListService = FakeRoomListService()
|
||||
val matrixClient = FakeMatrixClient(
|
||||
roomListService = roomListService,
|
||||
)
|
||||
fun `present - handle RecoveryKeyConfirmation last session`() = runTest {
|
||||
val scope = CoroutineScope(context = coroutineContext + SupervisorJob())
|
||||
val presenter = createRoomListPresenter(
|
||||
coroutineScope = scope,
|
||||
client = FakeMatrixClient(
|
||||
encryptionService = FakeEncryptionService().apply {
|
||||
emitIsLastDevice(true)
|
||||
}
|
||||
),
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
skipItems(1)
|
||||
val eventSink = awaitItem().eventSink
|
||||
// For the last session, the state is not SessionVerification, but RecoveryKeyConfirmation
|
||||
assertThat(awaitItem().securityBannerState).isEqualTo(SecurityBannerState.RecoveryKeyConfirmation)
|
||||
eventSink(RoomListEvents.DismissRequestVerificationPrompt)
|
||||
assertThat(awaitItem().securityBannerState).isEqualTo(SecurityBannerState.None)
|
||||
scope.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - handle DismissRequestVerificationPrompt`() = runTest {
|
||||
val scope = CoroutineScope(context = coroutineContext + SupervisorJob())
|
||||
val presenter = createRoomListPresenter(
|
||||
client = matrixClient,
|
||||
sessionVerificationService = FakeSessionVerificationService().apply {
|
||||
givenIsReady(true)
|
||||
givenVerifiedStatus(SessionVerifiedStatus.NotVerified)
|
||||
},
|
||||
coroutineScope = scope,
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val eventSink = awaitItem().eventSink
|
||||
assertThat(awaitItem().displayVerificationPrompt).isTrue()
|
||||
|
||||
assertThat(awaitItem().securityBannerState).isEqualTo(SecurityBannerState.SessionVerification)
|
||||
eventSink(RoomListEvents.DismissRequestVerificationPrompt)
|
||||
assertThat(awaitItem().displayVerificationPrompt).isFalse()
|
||||
assertThat(awaitItem().securityBannerState).isEqualTo(SecurityBannerState.None)
|
||||
scope.cancel()
|
||||
}
|
||||
}
|
||||
|
|
@ -271,6 +284,10 @@ class RoomListPresenterTests {
|
|||
val encryptionService = FakeEncryptionService()
|
||||
val matrixClient = FakeMatrixClient(
|
||||
encryptionService = encryptionService,
|
||||
sessionVerificationService = FakeSessionVerificationService().apply {
|
||||
givenCanVerifySession(false)
|
||||
},
|
||||
syncService = FakeSyncService(initialState = SyncState.Running)
|
||||
)
|
||||
val scope = CoroutineScope(context = coroutineContext + SupervisorJob())
|
||||
val presenter = createRoomListPresenter(
|
||||
|
|
@ -282,13 +299,13 @@ class RoomListPresenterTests {
|
|||
}.test {
|
||||
skipItems(1)
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.displayRecoveryKeyPrompt).isFalse()
|
||||
assertThat(initialState.securityBannerState).isEqualTo(SecurityBannerState.None)
|
||||
encryptionService.emitRecoveryState(RecoveryState.INCOMPLETE)
|
||||
val nextState = awaitItem()
|
||||
assertThat(nextState.displayRecoveryKeyPrompt).isTrue()
|
||||
assertThat(nextState.securityBannerState).isEqualTo(SecurityBannerState.RecoveryKeyConfirmation)
|
||||
nextState.eventSink(RoomListEvents.DismissRecoveryKeyPrompt)
|
||||
val finalState = awaitItem()
|
||||
assertThat(finalState.displayRecoveryKeyPrompt).isFalse()
|
||||
assertThat(finalState.securityBannerState).isEqualTo(SecurityBannerState.None)
|
||||
scope.cancel()
|
||||
}
|
||||
}
|
||||
|
|
@ -581,7 +598,6 @@ class RoomListPresenterTests {
|
|||
|
||||
private fun TestScope.createRoomListPresenter(
|
||||
client: MatrixClient = FakeMatrixClient(),
|
||||
sessionVerificationService: SessionVerificationService = FakeSessionVerificationService(),
|
||||
networkMonitor: NetworkMonitor = FakeNetworkMonitor(),
|
||||
snackbarDispatcher: SnackbarDispatcher = SnackbarDispatcher(),
|
||||
inviteStateDataSource: InviteStateDataSource = FakeInviteDataSource(),
|
||||
|
|
@ -591,7 +607,7 @@ class RoomListPresenterTests {
|
|||
},
|
||||
roomLastMessageFormatter: RoomLastMessageFormatter = FakeRoomLastMessageFormatter(),
|
||||
sessionPreferencesStore: SessionPreferencesStore = InMemorySessionPreferencesStore(),
|
||||
featureFlagService: FeatureFlagService = FakeFeatureFlagService(mapOf(FeatureFlags.SecureStorage.key to true)),
|
||||
featureFlagService: FeatureFlagService = FakeFeatureFlagService(),
|
||||
coroutineScope: CoroutineScope,
|
||||
migrationScreenPresenter: MigrationScreenPresenter = MigrationScreenPresenter(
|
||||
matrixClient = client,
|
||||
|
|
@ -602,7 +618,6 @@ class RoomListPresenterTests {
|
|||
searchPresenter: Presenter<RoomListSearchState> = Presenter { aRoomListSearchState() },
|
||||
) = RoomListPresenter(
|
||||
client = client,
|
||||
sessionVerificationService = sessionVerificationService,
|
||||
networkMonitor = networkMonitor,
|
||||
snackbarDispatcher = snackbarDispatcher,
|
||||
inviteStateDataSource = inviteStateDataSource,
|
||||
|
|
@ -619,9 +634,8 @@ class RoomListPresenterTests {
|
|||
),
|
||||
featureFlagService = featureFlagService,
|
||||
indicatorService = DefaultIndicatorService(
|
||||
sessionVerificationService = sessionVerificationService,
|
||||
sessionVerificationService = client.sessionVerificationService(),
|
||||
encryptionService = client.encryptionService(),
|
||||
featureFlagService = featureFlagService,
|
||||
),
|
||||
migrationScreenPresenter = migrationScreenPresenter,
|
||||
searchPresenter = searchPresenter,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,210 @@
|
|||
/*
|
||||
* Copyright (c) 2024 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.roomlist.impl
|
||||
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
|
||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||
import androidx.compose.ui.test.longClick
|
||||
import androidx.compose.ui.test.onNodeWithContentDescription
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.performClick
|
||||
import androidx.compose.ui.test.performTouchInput
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import io.element.android.features.roomlist.impl.components.RoomListMenuAction
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
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
|
||||
import io.element.android.tests.testutils.ensureCalledOnceWithParam
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.rules.TestRule
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class RoomListViewTest {
|
||||
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
|
||||
|
||||
@Test
|
||||
fun `clicking on close verification banner emits the expected Event`() {
|
||||
val eventsRecorder = EventsRecorder<RoomListEvents>()
|
||||
rule.setRoomListView(
|
||||
state = aRoomListState(
|
||||
securityBannerState = SecurityBannerState.SessionVerification,
|
||||
eventSink = eventsRecorder,
|
||||
)
|
||||
)
|
||||
val close = rule.activity.getString(CommonStrings.action_close)
|
||||
rule.onNodeWithContentDescription(close).performClick()
|
||||
eventsRecorder.assertSingle(RoomListEvents.DismissRequestVerificationPrompt)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clicking on continue verification banner invokes the expected callback`() {
|
||||
val eventsRecorder = EventsRecorder<RoomListEvents>(expectEvents = false)
|
||||
ensureCalledOnce { callback ->
|
||||
rule.setRoomListView(
|
||||
state = aRoomListState(
|
||||
securityBannerState = SecurityBannerState.SessionVerification,
|
||||
eventSink = eventsRecorder,
|
||||
),
|
||||
onVerifyClicked = callback,
|
||||
)
|
||||
rule.clickOn(CommonStrings.action_continue)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clicking on close recovery key banner emits the expected Event`() {
|
||||
val eventsRecorder = EventsRecorder<RoomListEvents>()
|
||||
rule.setRoomListView(
|
||||
state = aRoomListState(
|
||||
securityBannerState = SecurityBannerState.RecoveryKeyConfirmation,
|
||||
eventSink = eventsRecorder,
|
||||
)
|
||||
)
|
||||
val close = rule.activity.getString(CommonStrings.action_close)
|
||||
rule.onNodeWithContentDescription(close).performClick()
|
||||
eventsRecorder.assertSingle(RoomListEvents.DismissRecoveryKeyPrompt)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clicking on continue recovery key banner invokes the expected callback`() {
|
||||
val eventsRecorder = EventsRecorder<RoomListEvents>(expectEvents = false)
|
||||
ensureCalledOnce { callback ->
|
||||
rule.setRoomListView(
|
||||
state = aRoomListState(
|
||||
securityBannerState = SecurityBannerState.RecoveryKeyConfirmation,
|
||||
eventSink = eventsRecorder,
|
||||
),
|
||||
onConfirmRecoveryKeyClicked = callback,
|
||||
)
|
||||
rule.clickOn(CommonStrings.action_continue)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clicking on start chat when the session has no room invokes the expected callback`() {
|
||||
val eventsRecorder = EventsRecorder<RoomListEvents>(expectEvents = false)
|
||||
ensureCalledOnce { callback ->
|
||||
rule.setRoomListView(
|
||||
state = aRoomListState(
|
||||
eventSink = eventsRecorder,
|
||||
roomList = AsyncData.Success(persistentListOf()),
|
||||
),
|
||||
onCreateRoomClicked = callback,
|
||||
)
|
||||
rule.clickOn(CommonStrings.action_start_chat)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clicking on a room invokes the expected callback`() {
|
||||
val eventsRecorder = EventsRecorder<RoomListEvents>(expectEvents = false)
|
||||
val state = aRoomListState(
|
||||
eventSink = eventsRecorder,
|
||||
)
|
||||
val room0 = state.roomList.dataOrNull()!!.first()
|
||||
ensureCalledOnceWithParam(room0.roomId) { callback ->
|
||||
rule.setRoomListView(
|
||||
state = state,
|
||||
onRoomClicked = callback,
|
||||
)
|
||||
rule.onNodeWithText(room0.lastMessage!!.toString()).performClick()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `long clicking on a room emits the expected Event`() {
|
||||
val eventsRecorder = EventsRecorder<RoomListEvents>()
|
||||
val state = aRoomListState(
|
||||
eventSink = eventsRecorder,
|
||||
)
|
||||
val room0 = state.roomList.dataOrNull()!!.first()
|
||||
rule.setRoomListView(
|
||||
state = state,
|
||||
)
|
||||
rule.onNodeWithText(room0.lastMessage!!.toString()).performTouchInput { longClick() }
|
||||
eventsRecorder.assertSingle(RoomListEvents.ShowContextMenu(room0))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clicking on a room setting invokes the expected callback and emits expected Event`() {
|
||||
val eventsRecorder = EventsRecorder<RoomListEvents>()
|
||||
val state = aRoomListState(
|
||||
contextMenu = aContextMenuShown(),
|
||||
eventSink = eventsRecorder,
|
||||
)
|
||||
val room0 = (state.contextMenu as RoomListState.ContextMenu.Shown).roomId
|
||||
ensureCalledOnceWithParam(room0) { callback ->
|
||||
rule.setRoomListView(
|
||||
state = state,
|
||||
onRoomSettingsClicked = callback,
|
||||
)
|
||||
rule.clickOn(CommonStrings.common_settings)
|
||||
}
|
||||
eventsRecorder.assertSingle(RoomListEvents.HideContextMenu)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clicking on invites invokes the expected callback`() {
|
||||
val eventsRecorder = EventsRecorder<RoomListEvents>()
|
||||
val state = aRoomListState(
|
||||
invitesState = InvitesState.NewInvites,
|
||||
eventSink = eventsRecorder,
|
||||
)
|
||||
ensureCalledOnce { callback ->
|
||||
rule.setRoomListView(
|
||||
state = state,
|
||||
onInvitesClicked = callback,
|
||||
)
|
||||
rule.clickOn(CommonStrings.action_invites_list)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setRoomListView(
|
||||
state: RoomListState,
|
||||
onRoomClicked: (RoomId) -> Unit = EnsureNeverCalledWithParam(),
|
||||
onSettingsClicked: () -> Unit = EnsureNeverCalled(),
|
||||
onVerifyClicked: () -> Unit = EnsureNeverCalled(),
|
||||
onConfirmRecoveryKeyClicked: () -> Unit = EnsureNeverCalled(),
|
||||
onCreateRoomClicked: () -> Unit = EnsureNeverCalled(),
|
||||
onInvitesClicked: () -> Unit = EnsureNeverCalled(),
|
||||
onRoomSettingsClicked: (RoomId) -> Unit = EnsureNeverCalledWithParam(),
|
||||
onMenuActionClicked: (RoomListMenuAction) -> Unit = EnsureNeverCalledWithParam(),
|
||||
) {
|
||||
setContent {
|
||||
RoomListView(
|
||||
state = state,
|
||||
onRoomClicked = onRoomClicked,
|
||||
onSettingsClicked = onSettingsClicked,
|
||||
onVerifyClicked = onVerifyClicked,
|
||||
onConfirmRecoveryKeyClicked = onConfirmRecoveryKeyClicked,
|
||||
onCreateRoomClicked = onCreateRoomClicked,
|
||||
onInvitesClicked = onInvitesClicked,
|
||||
onRoomSettingsClicked = onRoomSettingsClicked,
|
||||
onMenuActionClicked = onMenuActionClicked,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
plugins {
|
||||
id("io.element.android-library")
|
||||
id("kotlin-parcelize")
|
||||
}
|
||||
|
||||
android {
|
||||
|
|
|
|||
|
|
@ -16,6 +16,28 @@
|
|||
|
||||
package io.element.android.features.securebackup.api
|
||||
|
||||
import io.element.android.libraries.architecture.SimpleFeatureEntryPoint
|
||||
import android.os.Parcelable
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import io.element.android.libraries.architecture.FeatureEntryPoint
|
||||
import io.element.android.libraries.architecture.NodeInputs
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
interface SecureBackupEntryPoint : SimpleFeatureEntryPoint
|
||||
interface SecureBackupEntryPoint : FeatureEntryPoint {
|
||||
sealed interface InitialTarget : Parcelable {
|
||||
@Parcelize
|
||||
data object Root : InitialTarget
|
||||
|
||||
@Parcelize
|
||||
data object EnterRecoveryKey : InitialTarget
|
||||
}
|
||||
|
||||
data class Params(val initialElement: InitialTarget) : NodeInputs
|
||||
|
||||
fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder
|
||||
|
||||
interface NodeBuilder {
|
||||
fun params(params: Params): NodeBuilder
|
||||
fun build(): Node
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ package io.element.android.features.securebackup.impl
|
|||
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.features.securebackup.api.SecureBackupEntryPoint
|
||||
import io.element.android.libraries.architecture.createNode
|
||||
|
|
@ -26,7 +27,18 @@ import javax.inject.Inject
|
|||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultSecureBackupEntryPoint @Inject constructor() : SecureBackupEntryPoint {
|
||||
override fun createNode(parentNode: Node, buildContext: BuildContext): Node {
|
||||
return parentNode.createNode<SecureBackupFlowNode>(buildContext)
|
||||
override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): SecureBackupEntryPoint.NodeBuilder {
|
||||
val plugins = ArrayList<Plugin>()
|
||||
|
||||
return object : SecureBackupEntryPoint.NodeBuilder {
|
||||
override fun params(params: SecureBackupEntryPoint.Params): SecureBackupEntryPoint.NodeBuilder {
|
||||
plugins += params
|
||||
return this
|
||||
}
|
||||
|
||||
override fun build(): Node {
|
||||
return parentNode.createNode<SecureBackupFlowNode>(buildContext, plugins)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ 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.features.securebackup.api.SecureBackupEntryPoint
|
||||
import io.element.android.features.securebackup.impl.disable.SecureBackupDisableNode
|
||||
import io.element.android.features.securebackup.impl.enable.SecureBackupEnableNode
|
||||
import io.element.android.features.securebackup.impl.enter.SecureBackupEnterRecoveryKeyNode
|
||||
|
|
@ -44,7 +45,10 @@ class SecureBackupFlowNode @AssistedInject constructor(
|
|||
@Assisted plugins: List<Plugin>,
|
||||
) : BaseFlowNode<SecureBackupFlowNode.NavTarget>(
|
||||
backstack = BackStack(
|
||||
initialElement = NavTarget.Root,
|
||||
initialElement = when (plugins.filterIsInstance(SecureBackupEntryPoint.Params::class.java).first().initialElement) {
|
||||
SecureBackupEntryPoint.InitialTarget.Root -> NavTarget.Root
|
||||
SecureBackupEntryPoint.InitialTarget.EnterRecoveryKey -> NavTarget.EnterRecoveryKey
|
||||
},
|
||||
savedStateMap = buildContext.savedStateMap,
|
||||
),
|
||||
buildContext = buildContext,
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
<string name="screen_chat_backup_key_backup_description">"Backup ensures that you don\'t lose your message history. %1$s."</string>
|
||||
<string name="screen_chat_backup_key_backup_title">"Backup"</string>
|
||||
<string name="screen_chat_backup_recovery_action_change">"Change recovery key"</string>
|
||||
<string name="screen_chat_backup_recovery_action_confirm">"Confirm recovery key"</string>
|
||||
<string name="screen_chat_backup_recovery_action_confirm">"Enter recovery key"</string>
|
||||
<string name="screen_chat_backup_recovery_action_confirm_description">"Your chat backup is currently out of sync."</string>
|
||||
<string name="screen_chat_backup_recovery_action_setup">"Set up recovery"</string>
|
||||
<string name="screen_chat_backup_recovery_action_setup_description">"Get access to your encrypted messages if you lose all your devices or are signed out of %1$s everywhere."</string>
|
||||
|
|
@ -27,7 +27,7 @@
|
|||
<string name="screen_recovery_key_confirm_key_description">"Enter the 48 character code."</string>
|
||||
<string name="screen_recovery_key_confirm_key_placeholder">"Enter…"</string>
|
||||
<string name="screen_recovery_key_confirm_success">"Recovery key confirmed"</string>
|
||||
<string name="screen_recovery_key_confirm_title">"Confirm your recovery key"</string>
|
||||
<string name="screen_recovery_key_confirm_title">"Enter your recovery key"</string>
|
||||
<string name="screen_recovery_key_copied_to_clipboard">"Copied recovery key"</string>
|
||||
<string name="screen_recovery_key_generating_key">"Generating…"</string>
|
||||
<string name="screen_recovery_key_save_action">"Save recovery key"</string>
|
||||
|
|
|
|||
|
|
@ -16,6 +16,20 @@
|
|||
|
||||
package io.element.android.features.verifysession.api
|
||||
|
||||
import io.element.android.libraries.architecture.SimpleFeatureEntryPoint
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import io.element.android.libraries.architecture.FeatureEntryPoint
|
||||
|
||||
interface VerifySessionEntryPoint : SimpleFeatureEntryPoint
|
||||
interface VerifySessionEntryPoint : FeatureEntryPoint {
|
||||
fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder
|
||||
|
||||
interface NodeBuilder {
|
||||
fun callback(callback: Callback): NodeBuilder
|
||||
fun build(): Node
|
||||
}
|
||||
|
||||
interface Callback : Plugin {
|
||||
fun onEnterRecoveryKey()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,11 @@ plugins {
|
|||
|
||||
android {
|
||||
namespace = "io.element.android.features.verifysession.impl"
|
||||
testOptions {
|
||||
unitTests {
|
||||
isIncludeAndroidResources = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
anvil {
|
||||
|
|
@ -44,10 +49,13 @@ dependencies {
|
|||
testImplementation(libs.test.junit)
|
||||
testImplementation(libs.coroutines.test)
|
||||
testImplementation(libs.molecule.runtime)
|
||||
testImplementation(libs.test.robolectric)
|
||||
testImplementation(libs.test.truth)
|
||||
testImplementation(libs.test.turbine)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(projects.tests.testutils)
|
||||
testImplementation(libs.androidx.compose.ui.test.junit)
|
||||
testReleaseImplementation(libs.androidx.compose.ui.test.manifest)
|
||||
|
||||
ksp(libs.showkase.processor)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ package io.element.android.features.verifysession.impl
|
|||
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.features.verifysession.api.VerifySessionEntryPoint
|
||||
import io.element.android.libraries.architecture.createNode
|
||||
|
|
@ -26,7 +27,18 @@ import javax.inject.Inject
|
|||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultVerifySessionEntryPoint @Inject constructor() : VerifySessionEntryPoint {
|
||||
override fun createNode(parentNode: Node, buildContext: BuildContext): Node {
|
||||
return parentNode.createNode<VerifySelfSessionNode>(buildContext)
|
||||
override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): VerifySessionEntryPoint.NodeBuilder {
|
||||
val plugins = ArrayList<Plugin>()
|
||||
|
||||
return object : VerifySessionEntryPoint.NodeBuilder {
|
||||
override fun callback(callback: VerifySessionEntryPoint.Callback): VerifySessionEntryPoint.NodeBuilder {
|
||||
plugins += callback
|
||||
return this
|
||||
}
|
||||
|
||||
override fun build(): Node {
|
||||
return parentNode.createNode<VerifySelfSessionNode>(buildContext, plugins)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,9 +21,11 @@ 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.verifysession.api.VerifySessionEntryPoint
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
|
||||
@ContributesNode(SessionScope::class)
|
||||
|
|
@ -32,12 +34,19 @@ class VerifySelfSessionNode @AssistedInject constructor(
|
|||
@Assisted plugins: List<Plugin>,
|
||||
private val presenter: VerifySelfSessionPresenter,
|
||||
) : Node(buildContext, plugins = plugins) {
|
||||
private fun onEnterRecoveryKey() {
|
||||
plugins<VerifySessionEntryPoint.Callback>().forEach {
|
||||
it.onEnterRecoveryKey()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
val state = presenter.present()
|
||||
VerifySelfSessionView(
|
||||
state = state,
|
||||
modifier = modifier,
|
||||
onEnterRecoveryKey = { onEnterRecoveryKey() },
|
||||
goBack = { navigateUp() }
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,12 +20,15 @@ package io.element.android.features.verifysession.impl
|
|||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
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.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.VerificationFlowState
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
|
@ -38,6 +41,7 @@ import io.element.android.features.verifysession.impl.VerifySelfSessionStateMach
|
|||
|
||||
class VerifySelfSessionPresenter @Inject constructor(
|
||||
private val sessionVerificationService: SessionVerificationService,
|
||||
private val encryptionService: EncryptionService,
|
||||
private val stateMachine: VerifySelfSessionStateMachine,
|
||||
) : Presenter<VerifySelfSessionState> {
|
||||
@Composable
|
||||
|
|
@ -46,9 +50,14 @@ class VerifySelfSessionPresenter @Inject constructor(
|
|||
// 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()
|
||||
val verificationFlowStep by remember {
|
||||
derivedStateOf { stateAndDispatch.state.value.toVerificationStep() }
|
||||
derivedStateOf {
|
||||
stateAndDispatch.state.value.toVerificationStep(
|
||||
canEnterRecoveryKey = recoveryState == RecoveryState.INCOMPLETE
|
||||
)
|
||||
}
|
||||
}
|
||||
// Start this after observing state machine
|
||||
LaunchedEffect(Unit) {
|
||||
|
|
@ -71,10 +80,12 @@ class VerifySelfSessionPresenter @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
private fun StateMachineState?.toVerificationStep(): VerifySelfSessionState.VerificationStep =
|
||||
private fun StateMachineState?.toVerificationStep(
|
||||
canEnterRecoveryKey: Boolean
|
||||
): VerifySelfSessionState.VerificationStep =
|
||||
when (val machineState = this) {
|
||||
StateMachineState.Initial, null -> {
|
||||
VerifySelfSessionState.VerificationStep.Initial
|
||||
VerifySelfSessionState.VerificationStep.Initial(canEnterRecoveryKey = canEnterRecoveryKey)
|
||||
}
|
||||
StateMachineState.RequestingVerification,
|
||||
StateMachineState.StartingSasVerification,
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ data class VerifySelfSessionState(
|
|||
) {
|
||||
@Stable
|
||||
sealed interface VerificationStep {
|
||||
data object Initial : VerificationStep
|
||||
data class Initial(val canEnterRecoveryKey: Boolean) : VerificationStep
|
||||
data object Canceled : VerificationStep
|
||||
data object AwaitingOtherDeviceResponse : VerificationStep
|
||||
data object Ready : VerificationStep
|
||||
|
|
|
|||
|
|
@ -25,29 +25,32 @@ open class VerifySelfSessionStateProvider : PreviewParameterProvider<VerifySelfS
|
|||
override val values: Sequence<VerifySelfSessionState>
|
||||
get() = sequenceOf(
|
||||
aVerifySelfSessionState(),
|
||||
aVerifySelfSessionState().copy(
|
||||
aVerifySelfSessionState(
|
||||
verificationFlowStep = VerifySelfSessionState.VerificationStep.AwaitingOtherDeviceResponse
|
||||
),
|
||||
aVerifySelfSessionState().copy(
|
||||
aVerifySelfSessionState(
|
||||
verificationFlowStep = VerifySelfSessionState.VerificationStep.Verifying(aEmojisSessionVerificationData(), AsyncData.Uninitialized)
|
||||
),
|
||||
aVerifySelfSessionState().copy(
|
||||
aVerifySelfSessionState(
|
||||
verificationFlowStep = VerifySelfSessionState.VerificationStep.Verifying(aEmojisSessionVerificationData(), AsyncData.Loading())
|
||||
),
|
||||
aVerifySelfSessionState().copy(
|
||||
aVerifySelfSessionState(
|
||||
verificationFlowStep = VerifySelfSessionState.VerificationStep.Canceled
|
||||
),
|
||||
aVerifySelfSessionState().copy(
|
||||
aVerifySelfSessionState(
|
||||
verificationFlowStep = VerifySelfSessionState.VerificationStep.Ready
|
||||
),
|
||||
aVerifySelfSessionState().copy(
|
||||
aVerifySelfSessionState(
|
||||
verificationFlowStep = VerifySelfSessionState.VerificationStep.Verifying(aDecimalsSessionVerificationData(), AsyncData.Uninitialized)
|
||||
),
|
||||
aVerifySelfSessionState(
|
||||
verificationFlowStep = VerifySelfSessionState.VerificationStep.Initial(true)
|
||||
),
|
||||
// Add other state here
|
||||
)
|
||||
}
|
||||
|
||||
private fun aEmojisSessionVerificationData(
|
||||
internal fun aEmojisSessionVerificationData(
|
||||
emojiList: List<VerificationEmoji> = aVerificationEmojiList(),
|
||||
): SessionVerificationData {
|
||||
return SessionVerificationData.Emojis(emojiList)
|
||||
|
|
@ -59,9 +62,12 @@ private fun aDecimalsSessionVerificationData(
|
|||
return SessionVerificationData.Decimals(decimals)
|
||||
}
|
||||
|
||||
private fun aVerifySelfSessionState() = VerifySelfSessionState(
|
||||
verificationFlowStep = VerifySelfSessionState.VerificationStep.Initial,
|
||||
eventSink = {},
|
||||
internal fun aVerifySelfSessionState(
|
||||
verificationFlowStep: VerifySelfSessionState.VerificationStep = VerifySelfSessionState.VerificationStep.Initial(false),
|
||||
eventSink: (VerifySelfSessionViewEvents) -> Unit = {},
|
||||
) = VerifySelfSessionState(
|
||||
verificationFlowStep = verificationFlowStep,
|
||||
eventSink = eventSink,
|
||||
)
|
||||
|
||||
private fun aVerificationEmojiList() = listOf(
|
||||
|
|
|
|||
|
|
@ -61,8 +61,9 @@ import io.element.android.features.verifysession.impl.VerifySelfSessionState.Ver
|
|||
@Composable
|
||||
fun VerifySelfSessionView(
|
||||
state: VerifySelfSessionState,
|
||||
modifier: Modifier = Modifier,
|
||||
onEnterRecoveryKey: () -> Unit,
|
||||
goBack: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
fun goBackAndCancelIfNeeded() {
|
||||
state.eventSink(VerifySelfSessionViewEvents.CancelAndClose)
|
||||
|
|
@ -85,7 +86,11 @@ fun VerifySelfSessionView(
|
|||
},
|
||||
footer = {
|
||||
if (buttonsVisible) {
|
||||
BottomMenu(screenState = state, goBack = ::goBackAndCancelIfNeeded)
|
||||
BottomMenu(
|
||||
screenState = state,
|
||||
goBack = ::goBackAndCancelIfNeeded,
|
||||
onEnterRecoveryKey = onEnterRecoveryKey
|
||||
)
|
||||
}
|
||||
}
|
||||
) {
|
||||
|
|
@ -96,13 +101,13 @@ fun VerifySelfSessionView(
|
|||
@Composable
|
||||
private fun HeaderContent(verificationFlowStep: FlowStep) {
|
||||
val iconResourceId = when (verificationFlowStep) {
|
||||
FlowStep.Initial -> R.drawable.ic_verification_devices
|
||||
is FlowStep.Initial -> R.drawable.ic_verification_devices
|
||||
FlowStep.Canceled -> R.drawable.ic_verification_warning
|
||||
FlowStep.AwaitingOtherDeviceResponse -> R.drawable.ic_verification_waiting
|
||||
FlowStep.Ready, is FlowStep.Verifying, FlowStep.Completed -> R.drawable.ic_verification_emoji
|
||||
}
|
||||
val titleTextId = when (verificationFlowStep) {
|
||||
FlowStep.Initial -> R.string.screen_session_verification_open_existing_session_title
|
||||
is FlowStep.Initial -> R.string.screen_session_verification_open_existing_session_title
|
||||
FlowStep.Canceled -> CommonStrings.common_verification_cancelled
|
||||
FlowStep.AwaitingOtherDeviceResponse -> R.string.screen_session_verification_waiting_to_accept_title
|
||||
FlowStep.Ready,
|
||||
|
|
@ -113,7 +118,7 @@ private fun HeaderContent(verificationFlowStep: FlowStep) {
|
|||
}
|
||||
}
|
||||
val subtitleTextId = when (verificationFlowStep) {
|
||||
FlowStep.Initial -> R.string.screen_session_verification_open_existing_session_subtitle
|
||||
is FlowStep.Initial -> R.string.screen_session_verification_open_existing_session_subtitle
|
||||
FlowStep.Canceled -> R.string.screen_session_verification_cancelled_subtitle
|
||||
FlowStep.AwaitingOtherDeviceResponse -> R.string.screen_session_verification_waiting_to_accept_subtitle
|
||||
FlowStep.Ready -> R.string.screen_session_verification_ready_subtitle
|
||||
|
|
@ -136,7 +141,7 @@ private fun HeaderContent(verificationFlowStep: FlowStep) {
|
|||
private fun Content(flowState: FlowStep) {
|
||||
Column(Modifier.fillMaxHeight(), verticalArrangement = Arrangement.Center) {
|
||||
when (flowState) {
|
||||
FlowStep.Initial, FlowStep.Ready, FlowStep.Canceled, FlowStep.Completed -> Unit
|
||||
is FlowStep.Initial, FlowStep.Ready, FlowStep.Canceled, FlowStep.Completed -> Unit
|
||||
FlowStep.AwaitingOtherDeviceResponse -> ContentWaiting()
|
||||
is FlowStep.Verifying -> ContentVerifying(flowState)
|
||||
}
|
||||
|
|
@ -203,13 +208,17 @@ private fun EmojiItemView(emoji: VerificationEmoji, modifier: Modifier = Modifie
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun BottomMenu(screenState: VerifySelfSessionState, goBack: () -> Unit) {
|
||||
private fun BottomMenu(
|
||||
screenState: VerifySelfSessionState,
|
||||
onEnterRecoveryKey: () -> Unit,
|
||||
goBack: () -> Unit,
|
||||
) {
|
||||
val verificationViewState = screenState.verificationFlowStep
|
||||
val eventSink = screenState.eventSink
|
||||
|
||||
val isVerifying = (verificationViewState as? FlowStep.Verifying)?.state is AsyncData.Loading<Unit>
|
||||
val positiveButtonTitle = when (verificationViewState) {
|
||||
FlowStep.Initial -> R.string.screen_session_verification_positive_button_initial
|
||||
is FlowStep.Initial -> R.string.screen_session_verification_positive_button_initial
|
||||
FlowStep.Canceled -> R.string.screen_session_verification_positive_button_canceled
|
||||
is FlowStep.Verifying -> {
|
||||
if (isVerifying) {
|
||||
|
|
@ -222,7 +231,7 @@ private fun BottomMenu(screenState: VerifySelfSessionState, goBack: () -> Unit)
|
|||
else -> null
|
||||
}
|
||||
val negativeButtonTitle = when (verificationViewState) {
|
||||
FlowStep.Initial -> CommonStrings.action_cancel
|
||||
is FlowStep.Initial -> CommonStrings.action_cancel
|
||||
FlowStep.Canceled -> CommonStrings.action_cancel
|
||||
is FlowStep.Verifying -> R.string.screen_session_verification_they_dont_match
|
||||
else -> null
|
||||
|
|
@ -230,7 +239,7 @@ private fun BottomMenu(screenState: VerifySelfSessionState, goBack: () -> Unit)
|
|||
val negativeButtonEnabled = !isVerifying
|
||||
|
||||
val positiveButtonEvent = when (verificationViewState) {
|
||||
FlowStep.Initial -> VerifySelfSessionViewEvents.RequestVerification
|
||||
is FlowStep.Initial -> VerifySelfSessionViewEvents.RequestVerification
|
||||
FlowStep.Ready -> VerifySelfSessionViewEvents.StartSasVerification
|
||||
is FlowStep.Verifying -> if (!isVerifying) VerifySelfSessionViewEvents.ConfirmVerification else null
|
||||
FlowStep.Canceled -> VerifySelfSessionViewEvents.Restart
|
||||
|
|
@ -263,6 +272,17 @@ private fun BottomMenu(screenState: VerifySelfSessionState, goBack: () -> Unit)
|
|||
enabled = negativeButtonEnabled,
|
||||
)
|
||||
}
|
||||
if (verificationViewState is FlowStep.Initial && verificationViewState.canEnterRecoveryKey) {
|
||||
Text(
|
||||
text = stringResource(id = CommonStrings.common_or),
|
||||
color = ElementTheme.colors.textSecondary,
|
||||
)
|
||||
TextButton(
|
||||
text = stringResource(R.string.screen_session_verification_enter_recovery_key),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
onClick = onEnterRecoveryKey,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -271,6 +291,7 @@ private fun BottomMenu(screenState: VerifySelfSessionState, goBack: () -> Unit)
|
|||
internal fun VerifySelfSessionViewPreview(@PreviewParameter(VerifySelfSessionStateProvider::class) state: VerifySelfSessionState) = ElementPreview {
|
||||
VerifySelfSessionView(
|
||||
state = state,
|
||||
onEnterRecoveryKey = {},
|
||||
goBack = {},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,9 +23,13 @@ import app.cash.turbine.test
|
|||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.verifysession.impl.VerifySelfSessionState.VerificationStep
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
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.VerificationEmoji
|
||||
import io.element.android.libraries.matrix.api.verification.VerificationFlowState
|
||||
import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService
|
||||
import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
|
|
@ -44,7 +48,21 @@ class VerifySelfSessionPresenterTests {
|
|||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.Initial)
|
||||
assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.Initial(false))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - Initial state is received, can use recovery key`() = runTest {
|
||||
val presenter = createVerifySelfSessionPresenter(
|
||||
encryptionService = FakeEncryptionService().apply {
|
||||
emitRecoveryState(RecoveryState.INCOMPLETE)
|
||||
}
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.Initial(true))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -67,7 +85,7 @@ class VerifySelfSessionPresenterTests {
|
|||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.verificationFlowStep).isEqualTo(VerificationStep.Initial)
|
||||
assertThat(initialState.verificationFlowStep).isEqualTo(VerificationStep.Initial(false))
|
||||
val eventSink = initialState.eventSink
|
||||
eventSink(VerifySelfSessionViewEvents.StartSasVerification)
|
||||
// Await for other device response:
|
||||
|
|
@ -86,7 +104,7 @@ class VerifySelfSessionPresenterTests {
|
|||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.verificationFlowStep).isEqualTo(VerificationStep.Initial)
|
||||
assertThat(initialState.verificationFlowStep).isEqualTo(VerificationStep.Initial(false))
|
||||
val eventSink = initialState.eventSink
|
||||
eventSink(VerifySelfSessionViewEvents.CancelAndClose)
|
||||
expectNoEvents()
|
||||
|
|
@ -203,7 +221,7 @@ class VerifySelfSessionPresenterTests {
|
|||
sessionVerificationData: SessionVerificationData = SessionVerificationData.Emojis(emptyList()),
|
||||
): VerifySelfSessionState {
|
||||
var state = awaitItem()
|
||||
assertThat(state.verificationFlowStep).isEqualTo(VerificationStep.Initial)
|
||||
assertThat(state.verificationFlowStep).isEqualTo(VerificationStep.Initial(false))
|
||||
state.eventSink(VerifySelfSessionViewEvents.RequestVerification)
|
||||
// Await for other device response:
|
||||
state = awaitItem()
|
||||
|
|
@ -223,8 +241,13 @@ class VerifySelfSessionPresenterTests {
|
|||
}
|
||||
|
||||
private fun createVerifySelfSessionPresenter(
|
||||
service: FakeSessionVerificationService = FakeSessionVerificationService()
|
||||
service: SessionVerificationService = FakeSessionVerificationService(),
|
||||
encryptionService: EncryptionService = FakeEncryptionService(),
|
||||
): VerifySelfSessionPresenter {
|
||||
return VerifySelfSessionPresenter(service, VerifySelfSessionStateMachine(service))
|
||||
return VerifySelfSessionPresenter(
|
||||
sessionVerificationService = service,
|
||||
encryptionService = encryptionService,
|
||||
stateMachine = VerifySelfSessionStateMachine(service),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,151 @@
|
|||
/*
|
||||
* Copyright (c) 2024 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.verifysession.impl
|
||||
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
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.EventsRecorder
|
||||
import io.element.android.tests.testutils.clickOn
|
||||
import io.element.android.tests.testutils.ensureCalledOnce
|
||||
import io.element.android.tests.testutils.pressBackKey
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.annotation.Config
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class VerifySelfSessionViewTest {
|
||||
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
|
||||
|
||||
@Test
|
||||
fun `clicking on cancel calls the expected callback and emits the expected Event`() {
|
||||
val eventsRecorder = EventsRecorder<VerifySelfSessionViewEvents>()
|
||||
ensureCalledOnce { callback ->
|
||||
rule.setContent {
|
||||
VerifySelfSessionView(
|
||||
aVerifySelfSessionState(
|
||||
verificationFlowStep = VerifySelfSessionState.VerificationStep.Initial(true),
|
||||
eventSink = eventsRecorder
|
||||
),
|
||||
onEnterRecoveryKey = EnsureNeverCalled(),
|
||||
goBack = callback,
|
||||
)
|
||||
}
|
||||
rule.clickOn(CommonStrings.action_cancel)
|
||||
}
|
||||
eventsRecorder.assertSingle(VerifySelfSessionViewEvents.CancelAndClose)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clicking on back key calls the expected callback and emits the expected Event`() {
|
||||
val eventsRecorder = EventsRecorder<VerifySelfSessionViewEvents>()
|
||||
ensureCalledOnce { callback ->
|
||||
rule.setContent {
|
||||
VerifySelfSessionView(
|
||||
aVerifySelfSessionState(
|
||||
verificationFlowStep = VerifySelfSessionState.VerificationStep.Initial(true),
|
||||
eventSink = eventsRecorder
|
||||
),
|
||||
onEnterRecoveryKey = EnsureNeverCalled(),
|
||||
goBack = callback,
|
||||
)
|
||||
}
|
||||
rule.pressBackKey()
|
||||
}
|
||||
eventsRecorder.assertSingle(VerifySelfSessionViewEvents.CancelAndClose)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when flow is completed, the expected callback is invoked`() {
|
||||
val eventsRecorder = EventsRecorder<VerifySelfSessionViewEvents>(expectEvents = false)
|
||||
ensureCalledOnce { callback ->
|
||||
rule.setContent {
|
||||
VerifySelfSessionView(
|
||||
aVerifySelfSessionState(
|
||||
verificationFlowStep = VerifySelfSessionState.VerificationStep.Completed,
|
||||
eventSink = eventsRecorder
|
||||
),
|
||||
onEnterRecoveryKey = EnsureNeverCalled(),
|
||||
goBack = callback,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Config(qualifiers = "h1024dp")
|
||||
@Test
|
||||
fun `clicking on enter recovery key calls the expected callback`() {
|
||||
val eventsRecorder = EventsRecorder<VerifySelfSessionViewEvents>(expectEvents = false)
|
||||
ensureCalledOnce { callback ->
|
||||
rule.setContent {
|
||||
VerifySelfSessionView(
|
||||
aVerifySelfSessionState(
|
||||
verificationFlowStep = VerifySelfSessionState.VerificationStep.Initial(true),
|
||||
eventSink = eventsRecorder
|
||||
),
|
||||
onEnterRecoveryKey = callback,
|
||||
goBack = EnsureNeverCalled(),
|
||||
)
|
||||
}
|
||||
rule.clickOn(R.string.screen_session_verification_enter_recovery_key)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clicking on they match emits the expected event`() {
|
||||
val eventsRecorder = EventsRecorder<VerifySelfSessionViewEvents>()
|
||||
rule.setContent {
|
||||
VerifySelfSessionView(
|
||||
aVerifySelfSessionState(
|
||||
verificationFlowStep = VerifySelfSessionState.VerificationStep.Verifying(
|
||||
data = aEmojisSessionVerificationData(),
|
||||
state = AsyncData.Uninitialized,
|
||||
),
|
||||
eventSink = eventsRecorder
|
||||
),
|
||||
onEnterRecoveryKey = EnsureNeverCalled(),
|
||||
goBack = EnsureNeverCalled(),
|
||||
)
|
||||
}
|
||||
rule.clickOn(R.string.screen_session_verification_they_match)
|
||||
eventsRecorder.assertSingle(VerifySelfSessionViewEvents.ConfirmVerification)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clicking on they do not match emits the expected event`() {
|
||||
val eventsRecorder = EventsRecorder<VerifySelfSessionViewEvents>()
|
||||
rule.setContent {
|
||||
VerifySelfSessionView(
|
||||
aVerifySelfSessionState(
|
||||
verificationFlowStep = VerifySelfSessionState.VerificationStep.Verifying(
|
||||
data = aEmojisSessionVerificationData(),
|
||||
state = AsyncData.Uninitialized,
|
||||
),
|
||||
eventSink = eventsRecorder
|
||||
),
|
||||
onEnterRecoveryKey = EnsureNeverCalled(),
|
||||
goBack = EnsureNeverCalled(),
|
||||
)
|
||||
}
|
||||
rule.clickOn(R.string.screen_session_verification_they_dont_match)
|
||||
eventsRecorder.assertSingle(VerifySelfSessionViewEvents.DeclineVerification)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue