Skip to content

Commit

Permalink
PM-15624 Align handling of no network states with iOS app. (#4431)
Browse files Browse the repository at this point in the history
  • Loading branch information
dseverns-livefront authored Dec 6, 2024
1 parent 12dd865 commit 7b7c2da
Show file tree
Hide file tree
Showing 11 changed files with 217 additions and 107 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import com.x8bit.bitwarden.data.platform.repository.util.displayLabel
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenOutlinedButton
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenSelectionDialog
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog
Expand Down Expand Up @@ -74,6 +75,9 @@ fun OtherScreen(

OtherDialogs(
dialogState = state.dialogState,
onDismissRequest = remember(viewModel) {
{ viewModel.trySendAction(OtherAction.DismissDialog) }
},
)

val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
Expand Down Expand Up @@ -256,12 +260,19 @@ private fun ClearClipboardFrequencyRow(
@Composable
private fun OtherDialogs(
dialogState: OtherState.DialogState?,
onDismissRequest: () -> Unit,
) {
when (dialogState) {
is OtherState.DialogState.Loading -> BitwardenLoadingDialog(
text = dialogState.message(),
)

is OtherState.DialogState.Error -> BitwardenBasicDialog(
title = dialogState.title.invoke(),
message = dialogState.message(),
onDismissRequest = onDismissRequest,
)

null -> Unit
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import android.os.Parcelable
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.platform.manager.NetworkConnectionManager
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
import com.x8bit.bitwarden.data.platform.repository.model.ClearClipboardFrequency
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
Expand Down Expand Up @@ -34,6 +35,7 @@ class OtherViewModel @Inject constructor(
private val clock: Clock,
private val settingsRepo: SettingsRepository,
private val vaultRepo: VaultRepository,
private val networkConnectionManager: NetworkConnectionManager,
savedStateHandle: SavedStateHandle,
) : BaseViewModel<OtherState, OtherEvent, OtherAction>(
initialState = savedStateHandle[KEY_STATE]
Expand Down Expand Up @@ -70,6 +72,15 @@ class OtherViewModel @Inject constructor(
is OtherAction.ClearClipboardFrequencyChange -> handleClearClipboardFrequencyChanged(action)
OtherAction.SyncNowButtonClick -> handleSyncNowButtonClicked()
is OtherAction.Internal -> handleInternalAction(action)
OtherAction.DismissDialog -> handleDismissDialog()
}

private fun handleDismissDialog() {
mutableStateFlow.update {
it.copy(
dialogState = null,
)
}
}

private fun handleAllowScreenCaptureToggled(action: OtherAction.AllowScreenCaptureToggle) {
Expand All @@ -96,10 +107,21 @@ class OtherViewModel @Inject constructor(
}

private fun handleSyncNowButtonClicked() {
mutableStateFlow.update {
it.copy(dialogState = OtherState.DialogState.Loading(R.string.syncing.asText()))
if (networkConnectionManager.isNetworkConnected) {
mutableStateFlow.update {
it.copy(dialogState = OtherState.DialogState.Loading(R.string.syncing.asText()))
}
vaultRepo.sync(forced = true)
} else {
mutableStateFlow.update {
it.copy(
dialogState = OtherState.DialogState.Error(
R.string.internet_connection_required_title.asText(),
R.string.internet_connection_required_message.asText(),
),
)
}
}
vaultRepo.sync(forced = true)
}

private fun handleInternalAction(action: OtherAction.Internal) {
Expand Down Expand Up @@ -148,6 +170,15 @@ data class OtherState(
data class Loading(
val message: Text,
) : DialogState()

/**
* Represents an error dialog with the given [title] and [message].
*/
@Parcelize
data class Error(
val title: Text,
val message: Text,
) : DialogState()
}
}

Expand Down Expand Up @@ -203,6 +234,11 @@ sealed class OtherAction {
*/
data object SyncNowButtonClick : OtherAction()

/**
* Indicates that the dialog should be dismissed.
*/
data object DismissDialog : OtherAction()

/**
* Models actions that the [OtherViewModel] itself might send.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import com.x8bit.bitwarden.data.vault.repository.model.SendData
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
import com.x8bit.bitwarden.ui.platform.base.util.Text
import com.x8bit.bitwarden.ui.platform.base.util.asText
import com.x8bit.bitwarden.ui.platform.base.util.concat
import com.x8bit.bitwarden.ui.platform.components.model.IconRes
import com.x8bit.bitwarden.ui.tools.feature.send.util.toViewState
import com.x8bit.bitwarden.ui.vault.feature.item.VaultItemScreen
Expand Down Expand Up @@ -180,10 +179,15 @@ class SendViewModel @Inject constructor(
}
}

is DataState.Loaded -> {
is DataState.NoNetwork,
is DataState.Loaded,
-> {
val data = dataState
.data
?: SendData(sendViewList = emptyList())
mutableStateFlow.update {
it.copy(
viewState = dataState.data.toViewState(
viewState = data.toViewState(
baseWebSendUrl = environmentRepo
.environment
.environmentUrlData
Expand All @@ -201,23 +205,6 @@ class SendViewModel @Inject constructor(
}
}

is DataState.NoNetwork -> {
mutableStateFlow.update {
it.copy(
viewState = SendState.ViewState.Error(
message = R.string.internet_connection_required_title
.asText()
.concat(
" ".asText(),
R.string.internet_connection_required_message.asText(),
),
),
dialogState = null,
isRefreshing = false,
)
}
}

is DataState.Pending -> {
mutableStateFlow.update {
it.copy(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.model.PolicyInformation
import com.x8bit.bitwarden.data.auth.repository.model.UserState
import com.x8bit.bitwarden.data.platform.manager.NetworkConnectionManager
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager
Expand Down Expand Up @@ -70,6 +71,7 @@ class AddSendViewModel @Inject constructor(
private val specialCircumstanceManager: SpecialCircumstanceManager,
private val vaultRepo: VaultRepository,
private val policyManager: PolicyManager,
private val networkConnectionManager: NetworkConnectionManager,
) : BaseViewModel<AddSendState, AddSendEvent, AddSendAction>(
// We load the state from the savedStateHandle for testing purposes.
initialState = savedStateHandle[KEY_STATE] ?: run {
Expand Down Expand Up @@ -513,6 +515,17 @@ class AddSendViewModel @Inject constructor(
return@onContent
}
}
if (!networkConnectionManager.isNetworkConnected) {
mutableStateFlow.update {
it.copy(
dialogState = AddSendState.DialogState.Error(
title = R.string.internet_connection_required_title.asText(),
message = R.string.internet_connection_required_message.asText(),
),
)
}
return@onContent
}
mutableStateFlow.update {
it.copy(
dialogState = AddSendState.DialogState.Loading(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CreateCredentialReques
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2RegisterCredentialResult
import com.x8bit.bitwarden.data.autofill.fido2.model.UserVerificationRequirement
import com.x8bit.bitwarden.data.autofill.util.isActiveWithFido2Credentials
import com.x8bit.bitwarden.data.platform.manager.NetworkConnectionManager
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager
Expand All @@ -41,7 +42,6 @@ import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
import com.x8bit.bitwarden.ui.platform.base.util.BackgroundEvent
import com.x8bit.bitwarden.ui.platform.base.util.Text
import com.x8bit.bitwarden.ui.platform.base.util.asText
import com.x8bit.bitwarden.ui.platform.base.util.concat
import com.x8bit.bitwarden.ui.platform.manager.resource.ResourceManager
import com.x8bit.bitwarden.ui.tools.feature.generator.model.GeneratorMode
import com.x8bit.bitwarden.ui.vault.feature.addedit.model.CustomFieldAction
Expand Down Expand Up @@ -103,6 +103,7 @@ class VaultAddEditViewModel @Inject constructor(
private val resourceManager: ResourceManager,
private val clock: Clock,
private val organizationEventManager: OrganizationEventManager,
private val networkConnectionManager: NetworkConnectionManager,
) : BaseViewModel<VaultAddEditState, VaultAddEditEvent, VaultAddEditAction>(
// We load the state from the savedStateHandle for testing purposes.
initialState = savedStateHandle[KEY_STATE]
Expand Down Expand Up @@ -408,6 +409,14 @@ class VaultAddEditViewModel @Inject constructor(
message = R.string.select_one_collection.asText(),
)
return@onContent
} else if (
!networkConnectionManager.isNetworkConnected
) {
showErrorDialog(
title = R.string.internet_connection_required_title.asText(),
message = R.string.internet_connection_required_message.asText(),
)
return@onContent
}

mutableStateFlow.update {
Expand Down Expand Up @@ -1541,16 +1550,10 @@ class VaultAddEditViewModel @Inject constructor(
}

is DataState.NoNetwork -> {
mutableStateFlow.update {
it.copy(
viewState = VaultAddEditState.ViewState.Error(
message = R.string.internet_connection_required_title
.asText()
.concat(
" ".asText(),
R.string.internet_connection_required_message.asText(),
),
),
mutableStateFlow.update { currentState ->
currentState.determineContentState(
vaultData = vaultDataState.data,
userData = action.userData,
)
}
}
Expand All @@ -1567,25 +1570,33 @@ class VaultAddEditViewModel @Inject constructor(
}

private fun VaultAddEditState.determineContentState(
vaultData: VaultData,
vaultData: VaultData?,
userData: UserState?,
): VaultAddEditState {
val internalVaultData = vaultData
?: VaultData(
cipherViewList = emptyList(),
collectionViewList = emptyList(),
folderViewList = emptyList(),
sendViewList = emptyList(),
fido2CredentialAutofillViewList = null,
)
val isIndividualVaultDisabled = policyManager
.getActivePolicies(type = PolicyTypeJson.PERSONAL_OWNERSHIP)
.any()
return copy(
viewState = vaultData.cipherViewList
viewState = internalVaultData.cipherViewList
.find { it.id == vaultAddEditType.vaultItemId }
.validateCipherOrReturnErrorState(
currentAccount = userData?.activeAccount,
vaultAddEditType = vaultAddEditType,
) { currentAccount, cipherView ->

val canDelete = vaultData
val canDelete = internalVaultData
.collectionViewList
.hasDeletePermissionInAtLeastOneCollection(cipherView?.collectionIds)

val canAssignToCollections = vaultData
val canAssignToCollections = internalVaultData
.collectionViewList
.canAssignToCollections(cipherView?.collectionIds)

Expand All @@ -1603,8 +1614,8 @@ class VaultAddEditViewModel @Inject constructor(
)
?: viewState)
.appendFolderAndOwnerData(
folderViewList = vaultData.folderViewList,
collectionViewList = vaultData
folderViewList = internalVaultData.folderViewList,
collectionViewList = internalVaultData
.collectionViewList
.filter { !it.readOnly },
activeAccount = currentAccount,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -576,7 +576,10 @@ class VaultViewModel @Inject constructor(
)

is DataState.Loading -> vaultLoadingReceive()
is DataState.NoNetwork -> vaultNoNetworkReceive(vaultData = vaultData)
is DataState.NoNetwork -> vaultNoNetworkReceive(
vaultData = vaultData,
showSshKeys = showSshKeys,
)
is DataState.Pending -> vaultPendingReceive(vaultData = vaultData)
}
}
Expand All @@ -603,9 +606,16 @@ class VaultViewModel @Inject constructor(
),
)
}
updateVaultState(vaultData.data, showSshKeys)
}

private fun updateVaultState(
vaultData: VaultData,
showSshKeys: Boolean,
) {
mutableStateFlow.update {
it.copy(
viewState = vaultData.data.toViewState(
viewState = vaultData.toViewState(
baseIconUrl = state.baseIconUrl,
isIconLoadingDisabled = state.isIconLoadingDisabled,
isPremium = state.isPremium,
Expand All @@ -625,17 +635,19 @@ class VaultViewModel @Inject constructor(
mutableStateFlow.update { it.copy(viewState = VaultState.ViewState.Loading) }
}

private fun vaultNoNetworkReceive(vaultData: DataState.NoNetwork<VaultData>) {
mutableStateFlow.updateToErrorStateOrDialog(
baseIconUrl = state.baseIconUrl,
vaultData = vaultData.data,
vaultFilterType = vaultFilterTypeOrDefault,
isPremium = state.isPremium,
errorTitle = R.string.internet_connection_required_title.asText(),
isIconLoadingDisabled = state.isIconLoadingDisabled,
hasMasterPassword = state.hasMasterPassword,
errorMessage = R.string.internet_connection_required_message.asText(),
isRefreshing = false,
private fun vaultNoNetworkReceive(
vaultData: DataState.NoNetwork<VaultData>,
showSshKeys: Boolean,
) {
val data = vaultData.data ?: VaultData(
cipherViewList = emptyList(),
collectionViewList = emptyList(),
folderViewList = emptyList(),
sendViewList = emptyList(),
)
updateVaultState(
vaultData = data,
showSshKeys = showSshKeys,
)
}

Expand Down
Loading

0 comments on commit 7b7c2da

Please sign in to comment.