Introduce AsyncAction with a Confirmation state and use it for logout action.

This commit is contained in:
Benoit Marty 2024-01-04 15:51:12 +01:00
parent b9a7b11482
commit d953c979e1
12 changed files with 246 additions and 131 deletions

View file

@ -26,6 +26,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.runCatchingUpdatingState
import io.element.android.libraries.core.bool.orTrue
@ -50,8 +51,8 @@ class LogoutPresenter @Inject constructor(
@Composable
override fun present(): LogoutState {
val localCoroutineScope = rememberCoroutineScope()
val logoutAction: MutableState<Async<String?>> = remember {
mutableStateOf(Async.Uninitialized)
val logoutAction: MutableState<AsyncAction<String?>> = remember {
mutableStateOf(AsyncAction.Uninitialized)
}
val secureStorageFlag by featureFlagService.isFeatureEnabledFlow(FeatureFlags.SecureStorage)
@ -66,7 +67,6 @@ class LogoutPresenter @Inject constructor(
}
.collectAsState(initial = BackupUploadState.Unknown)
var showLogoutDialog by remember { mutableStateOf(false) }
var isLastSession by remember { mutableStateOf(false) }
LaunchedEffect(Unit) {
isLastSession = encryptionService.isLastDevice().getOrNull() ?: false
@ -88,16 +88,14 @@ class LogoutPresenter @Inject constructor(
fun handleEvents(event: LogoutEvents) {
when (event) {
is LogoutEvents.Logout -> {
if (showLogoutDialog || event.ignoreSdkError) {
showLogoutDialog = false
if (logoutAction.value.isConfirming() || event.ignoreSdkError) {
localCoroutineScope.logout(logoutAction, event.ignoreSdkError)
} else {
showLogoutDialog = true
logoutAction.value = AsyncAction.Confirming
}
}
LogoutEvents.CloseDialogs -> {
logoutAction.value = Async.Uninitialized
showLogoutDialog = false
logoutAction.value = AsyncAction.Uninitialized
}
}
}
@ -108,7 +106,6 @@ class LogoutPresenter @Inject constructor(
doesBackupExistOnServer = doesBackupExistOnServerAction.value.dataOrNull().orTrue(),
recoveryState = recoveryState,
backupUploadState = backupUploadState,
showConfirmationDialog = showLogoutDialog,
logoutAction = logoutAction.value,
eventSink = ::handleEvents
)
@ -121,7 +118,7 @@ class LogoutPresenter @Inject constructor(
}
private fun CoroutineScope.logout(
logoutAction: MutableState<Async<String?>>,
logoutAction: MutableState<AsyncAction<String?>>,
ignoreSdkError: Boolean,
) = launch {
suspend {

View file

@ -16,7 +16,7 @@
package io.element.android.features.logout.impl
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.architecture.AsyncAction
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.RecoveryState
@ -27,7 +27,6 @@ data class LogoutState(
val doesBackupExistOnServer: Boolean,
val recoveryState: RecoveryState,
val backupUploadState: BackupUploadState,
val showConfirmationDialog: Boolean,
val logoutAction: Async<String?>,
val logoutAction: AsyncAction<String?>,
val eventSink: (LogoutEvents) -> Unit,
)

View file

@ -17,7 +17,7 @@
package io.element.android.features.logout.impl
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.architecture.AsyncAction
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.RecoveryState
@ -30,9 +30,9 @@ open class LogoutStateProvider : PreviewParameterProvider<LogoutState> {
aLogoutState(isLastSession = true),
aLogoutState(isLastSession = false, backupUploadState = BackupUploadState.Uploading(66, 200)),
aLogoutState(isLastSession = true, backupUploadState = BackupUploadState.Done),
aLogoutState(showConfirmationDialog = true),
aLogoutState(logoutAction = Async.Loading()),
aLogoutState(logoutAction = Async.Failure(Exception("Failed to logout"))),
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),
@ -47,15 +47,13 @@ fun aLogoutState(
doesBackupExistOnServer: Boolean = true,
recoveryState: RecoveryState = RecoveryState.ENABLED,
backupUploadState: BackupUploadState = BackupUploadState.Unknown,
showConfirmationDialog: Boolean = false,
logoutAction: Async<String?> = Async.Uninitialized,
logoutAction: AsyncAction<String?> = AsyncAction.Uninitialized,
) = LogoutState(
isLastSession = isLastSession,
backupState = backupState,
doesBackupExistOnServer = doesBackupExistOnServer,
recoveryState = recoveryState,
backupUploadState = backupUploadState,
showConfirmationDialog = showConfirmationDialog,
logoutAction = logoutAction,
eventSink = {}
)

View file

@ -32,8 +32,7 @@ import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.features.logout.impl.tools.isBackingUp
import io.element.android.features.logout.impl.ui.LogoutActionDialog
import io.element.android.features.logout.impl.ui.LogoutConfirmationDialog
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.designsystem.atomic.pages.FlowStepPage
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
@ -79,20 +78,11 @@ fun LogoutView(
},
)
// Log out confirmation dialog
if (state.showConfirmationDialog) {
LogoutConfirmationDialog(
onSubmitClicked = {
eventSink(LogoutEvents.Logout(ignoreSdkError = false))
},
onDismiss = {
eventSink(LogoutEvents.CloseDialogs)
}
)
}
LogoutActionDialog(
state.logoutAction,
onConfirmClicked = {
eventSink(LogoutEvents.Logout(ignoreSdkError = false))
},
onForceLogoutClicked = {
eventSink(LogoutEvents.Logout(ignoreSdkError = true))
},
@ -148,13 +138,13 @@ private fun ColumnScope.Buttons(
)
}
val signOutSubmitRes = when {
logoutAction is Async.Loading -> R.string.screen_signout_in_progress_dialog_content
logoutAction is AsyncAction.Loading -> R.string.screen_signout_in_progress_dialog_content
state.backupUploadState.isBackingUp() -> CommonStrings.action_signout_anyway
else -> CommonStrings.action_signout
}
Button(
text = stringResource(id = signOutSubmitRes),
showProgress = logoutAction is Async.Loading,
showProgress = logoutAction is AsyncAction.Loading,
destructive = true,
modifier = Modifier
.fillMaxWidth()

View file

@ -30,7 +30,7 @@ import io.element.android.features.logout.api.direct.DirectLogoutEvents
import io.element.android.features.logout.api.direct.DirectLogoutPresenter
import io.element.android.features.logout.api.direct.DirectLogoutState
import io.element.android.features.logout.impl.tools.isBackingUp
import io.element.android.libraries.architecture.Async
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
@ -54,8 +54,8 @@ class DefaultDirectLogoutPresenter @Inject constructor(
override fun present(): DirectLogoutState {
val localCoroutineScope = rememberCoroutineScope()
val logoutAction: MutableState<Async<String?>> = remember {
mutableStateOf(Async.Uninitialized)
val logoutAction: MutableState<AsyncAction<String?>> = remember {
mutableStateOf(AsyncAction.Uninitialized)
}
val secureStorageFlag by featureFlagService.isFeatureEnabledFlow(FeatureFlags.SecureStorage)
@ -70,7 +70,6 @@ class DefaultDirectLogoutPresenter @Inject constructor(
}
.collectAsState(initial = BackupUploadState.Unknown)
var showLogoutDialog by remember { mutableStateOf(false) }
var isLastSession by remember { mutableStateOf(false) }
LaunchedEffect(Unit) {
isLastSession = encryptionService.isLastDevice().getOrNull() ?: false
@ -79,16 +78,14 @@ class DefaultDirectLogoutPresenter @Inject constructor(
fun handleEvents(event: DirectLogoutEvents) {
when (event) {
is DirectLogoutEvents.Logout -> {
if (showLogoutDialog || event.ignoreSdkError) {
showLogoutDialog = false
if (logoutAction.value.isConfirming() || event.ignoreSdkError) {
localCoroutineScope.logout(logoutAction, event.ignoreSdkError)
} else {
showLogoutDialog = true
logoutAction.value = AsyncAction.Confirming
}
}
DirectLogoutEvents.CloseDialogs -> {
logoutAction.value = Async.Uninitialized
showLogoutDialog = false
logoutAction.value = AsyncAction.Uninitialized
}
}
}
@ -96,14 +93,13 @@ class DefaultDirectLogoutPresenter @Inject constructor(
return DirectLogoutState(
canDoDirectSignOut = !isLastSession &&
!backupUploadState.isBackingUp(),
showConfirmationDialog = showLogoutDialog,
logoutAction = logoutAction.value,
eventSink = ::handleEvents
)
}
private fun CoroutineScope.logout(
logoutAction: MutableState<Async<String?>>,
logoutAction: MutableState<AsyncAction<String?>>,
ignoreSdkError: Boolean,
) = launch {
suspend {

View file

@ -22,7 +22,6 @@ import io.element.android.features.logout.api.direct.DirectLogoutEvents
import io.element.android.features.logout.api.direct.DirectLogoutState
import io.element.android.features.logout.api.direct.DirectLogoutView
import io.element.android.features.logout.impl.ui.LogoutActionDialog
import io.element.android.features.logout.impl.ui.LogoutConfirmationDialog
import io.element.android.libraries.di.SessionScope
import javax.inject.Inject
@ -34,20 +33,11 @@ class DefaultDirectLogoutView @Inject constructor() : DirectLogoutView {
onSuccessLogout: (logoutUrlResult: String?) -> Unit,
) {
val eventSink = state.eventSink
// Log out confirmation dialog
if (state.showConfirmationDialog) {
LogoutConfirmationDialog(
onSubmitClicked = {
eventSink(DirectLogoutEvents.Logout(ignoreSdkError = false))
},
onDismiss = {
eventSink(DirectLogoutEvents.CloseDialogs)
}
)
}
LogoutActionDialog(
state.logoutAction,
onConfirmClicked = {
eventSink(DirectLogoutEvents.Logout(ignoreSdkError = false))
},
onForceLogoutClicked = {
eventSink(DirectLogoutEvents.Logout(ignoreSdkError = true))
},

View file

@ -20,22 +20,30 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.res.stringResource
import io.element.android.features.logout.impl.R
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.designsystem.components.ProgressDialog
import io.element.android.libraries.designsystem.components.dialogs.RetryDialog
import io.element.android.libraries.ui.strings.CommonStrings
@Composable
fun LogoutActionDialog(
state: Async<String?>,
state: AsyncAction<String?>,
onConfirmClicked: () -> Unit,
onForceLogoutClicked: () -> Unit,
onDismissError: () -> Unit,
onDismissError: () -> Unit, // TODO Rename
onSuccessLogout: (String?) -> Unit,
) {
when (state) {
is Async.Loading ->
AsyncAction.Uninitialized ->
Unit
AsyncAction.Confirming ->
LogoutConfirmationDialog(
onSubmitClicked = onConfirmClicked,
onDismiss = onDismissError
)
is AsyncAction.Loading ->
ProgressDialog(text = stringResource(id = R.string.screen_signout_in_progress_dialog_content))
is Async.Failure ->
is AsyncAction.Failure ->
RetryDialog(
title = stringResource(id = CommonStrings.dialog_title_error),
content = stringResource(id = CommonStrings.error_unknown),
@ -43,9 +51,7 @@ fun LogoutActionDialog(
onRetry = onForceLogoutClicked,
onDismiss = onDismissError,
)
Async.Uninitialized ->
Unit
is Async.Success ->
is AsyncAction.Success ->
LaunchedEffect(state) {
onSuccessLogout(state.data)
}