BIT-1158: Add No Network states to Vault Screen (#391)

This commit is contained in:
Brian Yencho
2023-12-14 10:59:26 -06:00
committed by Álison Fernandes
parent 36e913b680
commit 0655f74479
5 changed files with 380 additions and 13 deletions

View File

@@ -0,0 +1,49 @@
package com.x8bit.bitwarden.ui.platform.components
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.x8bit.bitwarden.R
/**
* A Bitwarden-themed, re-usable error state.
*
* Note that when [onTryAgainClick] is absent, there will be no "Try again" button displayed.
*/
@Composable
fun BitwardenErrorContent(
message: String,
modifier: Modifier = Modifier,
onTryAgainClick: (() -> Unit)? = null,
) {
Column(
modifier = modifier,
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
text = message,
color = MaterialTheme.colorScheme.onSurface,
style = MaterialTheme.typography.bodyMedium,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth(),
)
onTryAgainClick?.let {
Spacer(modifier = Modifier.height(16.dp))
BitwardenTextButton(
label = stringResource(id = R.string.try_again),
onClick = it,
)
}
}
}

View File

@@ -25,11 +25,15 @@ import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
import com.x8bit.bitwarden.ui.platform.components.BasicDialogState
import com.x8bit.bitwarden.ui.platform.components.BitwardenAccountActionItem
import com.x8bit.bitwarden.ui.platform.components.BitwardenAccountSwitcher
import com.x8bit.bitwarden.ui.platform.components.BitwardenBasicDialog
import com.x8bit.bitwarden.ui.platform.components.BitwardenErrorContent
import com.x8bit.bitwarden.ui.platform.components.BitwardenMediumTopAppBar
import com.x8bit.bitwarden.ui.platform.components.BitwardenOverflowActionItem
import com.x8bit.bitwarden.ui.platform.components.BitwardenScaffold
@@ -125,6 +129,12 @@ fun VaultScreen(
trashClick = remember(viewModel) {
{ viewModel.trySendAction(VaultAction.TrashClick) }
},
tryAgainClick = remember(viewModel) {
{ viewModel.trySendAction(VaultAction.TryAgainClick) }
},
dialogDismiss = remember(viewModel) {
{ viewModel.trySendAction(VaultAction.DialogDismiss) }
},
)
}
@@ -151,6 +161,8 @@ private fun VaultScreenScaffold(
identityGroupClick: () -> Unit,
secureNoteGroupClick: () -> Unit,
trashClick: () -> Unit,
tryAgainClick: () -> Unit,
dialogDismiss: () -> Unit,
) {
var accountMenuVisible by rememberSaveable {
mutableStateOf(false)
@@ -165,6 +177,20 @@ private fun VaultScreenScaffold(
canScroll = { !accountMenuVisible },
)
when (val dialog = state.dialog) {
is VaultState.DialogState.Error -> {
BitwardenBasicDialog(
visibilityState = BasicDialogState.Shown(
title = dialog.title,
message = dialog.message,
),
onDismissRequest = dialogDismiss,
)
}
null -> Unit
}
BitwardenScaffold(
topBar = {
BitwardenMediumTopAppBar(
@@ -229,9 +255,15 @@ private fun VaultScreenScaffold(
modifier = modifier,
addItemClickAction = addItemClickAction,
)
is VaultState.ViewState.Error -> BitwardenErrorContent(
message = viewState.message(),
onTryAgainClick = tryAgainClick,
modifier = modifier
.padding(horizontal = 16.dp),
)
}
val context = LocalContext.current
BitwardenAccountSwitcher(
isVisible = accountMenuVisible,
accountSummaries = state.accountSummaries.toImmutableList(),

View File

@@ -95,6 +95,8 @@ class VaultViewModel @Inject constructor(
is VaultAction.SecureNoteGroupClick -> handleSecureNoteClick()
is VaultAction.TrashClick -> handleTrashClick()
is VaultAction.VaultItemClick -> handleVaultItemClick(action)
is VaultAction.TryAgainClick -> handleTryAgainClick()
is VaultAction.DialogDismiss -> handleDialogDismiss()
is VaultAction.Internal.UserStateUpdateReceive -> handleUserStateUpdateReceive(action)
is VaultAction.Internal.VaultDataReceive -> handleVaultDataReceive(action)
}
@@ -176,6 +178,16 @@ class VaultViewModel @Inject constructor(
sendEvent(VaultEvent.NavigateToVaultItem(action.vaultItem.id))
}
private fun handleTryAgainClick() {
vaultRepository.sync()
}
private fun handleDialogDismiss() {
mutableStateFlow.update {
it.copy(dialog = null)
}
}
private fun handleUserStateUpdateReceive(action: VaultAction.Internal.UserStateUpdateReceive) {
// Leave the current data alone if there is no UserState; we are in the process of logging
// out.
@@ -225,9 +237,27 @@ class VaultViewModel @Inject constructor(
}
private fun vaultNoNetworkReceive(vaultData: DataState.NoNetwork<VaultData>) {
// TODO update state to no network state BIT-1158
mutableStateFlow.update { it.copy(viewState = VaultState.ViewState.NoItems) }
sendEvent(VaultEvent.ShowToast(message = "Vault no network state not yet implemented"))
val title = R.string.internet_connection_required_title.asText()
val message = R.string.internet_connection_required_message.asText()
if (vaultData.data != null) {
mutableStateFlow.update {
it.copy(
viewState = vaultData.data.toViewState(),
dialog = VaultState.DialogState.Error(
title = title,
message = message,
),
)
}
} else {
mutableStateFlow.update {
it.copy(
viewState = VaultState.ViewState.Error(
message = message,
),
)
}
}
}
private fun vaultPendingReceive(vaultData: DataState.Pending<VaultData>) {
@@ -245,6 +275,7 @@ class VaultViewModel @Inject constructor(
* @property initials The initials to be displayed on the avatar.
* @property accountSummaries List of all the current accounts.
* @property viewState The specific view state representing loading, no items, or content state.
* @property dialog Information about any dialogs that may need to be displayed.
* @property isSwitchingAccounts Whether or not we are actively switching accounts.
*/
@Parcelize
@@ -253,6 +284,7 @@ data class VaultState(
val initials: String,
val accountSummaries: List<AccountSummary>,
val viewState: ViewState,
val dialog: DialogState? = null,
// Internal-use properties
val isSwitchingAccounts: Boolean = false,
) : Parcelable {
@@ -280,6 +312,15 @@ data class VaultState(
@Parcelize
data object NoItems : ViewState()
/**
* Represents a state where the [VaultScreen] is unable to display data due to an error
* retrieving it. The given [message] should be displayed.
*/
@Parcelize
data class Error(
val message: Text,
) : ViewState()
/**
* Content state for the [VaultScreen] showing the actual content or items.
*
@@ -433,6 +474,21 @@ data class VaultState(
}
}
/**
* Information about a dialog to display.
*/
sealed class DialogState : Parcelable {
/**
* Represents an error dialog with the given [title] and [message].
*/
@Parcelize
data class Error(
val title: Text,
val message: Text,
) : DialogState()
}
companion object {
/**
* The maximum number of no folder items that can be displayed before the UI creates a
@@ -572,6 +628,16 @@ sealed class VaultAction {
*/
data object TrashClick : VaultAction()
/**
* The user has requested that any visible dialogs are dismissed.
*/
data object DialogDismiss : VaultAction()
/**
* User clicked the Try Again button when there is an error displayed.
*/
data object TryAgainClick : VaultAction()
/**
* Models actions that the [VaultViewModel] itself might send.
*/