Skip to content

Commit

Permalink
BITAU-178 Show shared codes on the search screen (#264)
Browse files Browse the repository at this point in the history
Co-authored-by: Patrick Honkonen <[email protected]>
  • Loading branch information
ahaisting-livefront and SaintPatrck authored Oct 31, 2024
1 parent 5ac5e31 commit 78d7865
Show file tree
Hide file tree
Showing 7 changed files with 149 additions and 212 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.bitwarden.authenticator.data.authenticator.repository.util

import com.bitwarden.authenticator.data.authenticator.manager.model.VerificationCodeItem
import com.bitwarden.authenticator.data.authenticator.repository.model.SharedVerificationCodesState

/**
Expand All @@ -19,3 +20,19 @@ val SharedVerificationCodesState.isSyncWithBitwardenEnabled: Boolean

is SharedVerificationCodesState.Success -> true
}

/**
* Get a list of shared items, or empty if there are no shared items.
*/
val SharedVerificationCodesState.itemsOrEmpty: List<VerificationCodeItem>
get() = when (this) {
SharedVerificationCodesState.AppNotInstalled,
SharedVerificationCodesState.Error,
SharedVerificationCodesState.FeatureNotEnabled,
SharedVerificationCodesState.Loading,
SharedVerificationCodesState.OsVersionNotSupported,
SharedVerificationCodesState.SyncNotEnabled,
-> emptyList()

is SharedVerificationCodesState.Success -> this.items
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,6 @@ import com.bitwarden.authenticator.ui.platform.base.util.EventsEffect
import com.bitwarden.authenticator.ui.platform.base.util.bottomDivider
import com.bitwarden.authenticator.ui.platform.components.appbar.BitwardenSearchTopAppBar
import com.bitwarden.authenticator.ui.platform.components.appbar.NavigationIcon
import com.bitwarden.authenticator.ui.platform.components.content.BitwardenErrorContent
import com.bitwarden.authenticator.ui.platform.components.content.BitwardenLoadingContent
import com.bitwarden.authenticator.ui.platform.components.dialog.BasicDialogState
import com.bitwarden.authenticator.ui.platform.components.dialog.BitwardenBasicDialog
import com.bitwarden.authenticator.ui.platform.components.dialog.BitwardenLoadingDialog
import com.bitwarden.authenticator.ui.platform.components.dialog.LoadingDialogState
import com.bitwarden.authenticator.ui.platform.components.scaffold.BitwardenScaffold

/**
Expand Down Expand Up @@ -60,11 +54,6 @@ fun ItemSearchScreen(
}
}

ItemSearchDialogs(
dialogState = state.dialogState,
onDismissRequest = searchHandlers.onDismissRequest,
)

val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
BitwardenScaffold(
modifier = Modifier
Expand Down Expand Up @@ -110,47 +99,7 @@ fun ItemSearchScreen(
modifier = innerModifier,
)
}

is ItemSearchState.ViewState.Error -> {
BitwardenErrorContent(
message = viewState.message(),
modifier = innerModifier,
)
}

is ItemSearchState.ViewState.Loading -> {
BitwardenLoadingContent(
modifier = innerModifier,
)
}
}
}
}
}

/**
* Dialogs displayed within the context of the item search screen.
*/
@Composable
private fun ItemSearchDialogs(
dialogState: ItemSearchState.DialogState?,
onDismissRequest: () -> Unit,
) {
when (dialogState) {
is ItemSearchState.DialogState.Error -> {
BitwardenBasicDialog(
visibilityState = BasicDialogState.Shown(
title = dialogState.title,
message = dialogState.message,
),
onDismissRequest = onDismissRequest,
)
}

is ItemSearchState.DialogState.Loading -> {
BitwardenLoadingDialog(visibilityState = LoadingDialogState.Shown(dialogState.message))
}

null -> Unit
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,19 @@ import androidx.lifecycle.viewModelScope
import com.bitwarden.authenticator.R
import com.bitwarden.authenticator.data.authenticator.manager.model.VerificationCodeItem
import com.bitwarden.authenticator.data.authenticator.repository.AuthenticatorRepository
import com.bitwarden.authenticator.data.authenticator.repository.model.SharedVerificationCodesState
import com.bitwarden.authenticator.data.authenticator.repository.util.itemsOrEmpty
import com.bitwarden.authenticator.data.platform.manager.clipboard.BitwardenClipboardManager
import com.bitwarden.authenticator.data.platform.repository.model.DataState
import com.bitwarden.authenticator.data.platform.util.SpecialCharWithPrecedenceComparator
import com.bitwarden.authenticator.ui.platform.base.BaseViewModel
import com.bitwarden.authenticator.ui.platform.base.util.Text
import com.bitwarden.authenticator.ui.platform.base.util.asText
import com.bitwarden.authenticator.ui.platform.base.util.concat
import com.bitwarden.authenticator.ui.platform.base.util.removeDiacritics
import com.bitwarden.authenticator.ui.platform.components.model.IconData
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.update
import kotlinx.parcelize.Parcelize
Expand All @@ -38,15 +39,17 @@ class ItemSearchViewModel @Inject constructor(
initialState = savedStateHandle[KEY_STATE]
?: ItemSearchState(
searchTerm = "",
viewState = ItemSearchState.ViewState.Loading,
dialogState = null,
viewState = ItemSearchState.ViewState.Empty(message = null),
),
) {

init {
authenticatorRepository
.getLocalVerificationCodesFlow()
.map { ItemSearchAction.Internal.AuthenticatorDataReceive(it) }
combine(
authenticatorRepository.getLocalVerificationCodesFlow(),
authenticatorRepository.sharedCodesStateFlow,
) { localItems, sharedItems ->
ItemSearchAction.Internal.AuthenticatorDataReceive(localItems, sharedItems)
}
.onEach(::sendAction)
.launchIn(viewModelScope)
}
Expand All @@ -57,10 +60,6 @@ class ItemSearchViewModel @Inject constructor(
sendEvent(ItemSearchEvent.NavigateBack)
}

is ItemSearchAction.DismissDialogClick -> {
mutableStateFlow.update { it.copy(dialogState = null) }
}

is ItemSearchAction.SearchTermChange -> {
mutableStateFlow.update { it.copy(searchTerm = action.searchTerm) }
recalculateViewState()
Expand All @@ -84,108 +83,35 @@ class ItemSearchViewModel @Inject constructor(
private fun handleAuthenticatorDataReceive(
action: ItemSearchAction.Internal.AuthenticatorDataReceive,
) {
when (val data = action.dataState) {
is DataState.Error -> authenticatorErrorReceive(authenticatorData = data)
is DataState.Loaded -> authenticatorLoadedReceive(authenticatorData = data)
DataState.Loading -> authenticatorLoadingReceive()
is DataState.NoNetwork -> authenticatorNoNetworkReceive(authenticatorData = data)
is DataState.Pending -> authenticatorDataPendingReceive(authenticatorData = data)
action.localData.data?.let { localItems ->
val allItems = localItems + action.sharedData.itemsOrEmpty
updateStateWithAuthenticatorData(allItems)
}
}

private fun authenticatorErrorReceive(
authenticatorData: DataState<List<VerificationCodeItem>>,
) {
authenticatorData
.data
?.let {
updateStateWithAuthenticatorData(
authenticatorData = it,
clearDialogState = true,
)
}
?.run {
mutableStateFlow.update {
it.copy(
viewState = ItemSearchState.ViewState.Error(
message = R.string.generic_error_message.asText(),
),
)
}
}
}

private fun authenticatorLoadedReceive(
authenticatorData: DataState.Loaded<List<VerificationCodeItem>>,
) {
updateStateWithAuthenticatorData(
authenticatorData = authenticatorData.data,
clearDialogState = true,
)
}

private fun authenticatorLoadingReceive() {
mutableStateFlow.update { it.copy(viewState = ItemSearchState.ViewState.Loading) }
}

private fun authenticatorNoNetworkReceive(
authenticatorData: DataState<List<VerificationCodeItem>>,
) {
authenticatorData
.data
?.let {
updateStateWithAuthenticatorData(
authenticatorData = it,
clearDialogState = true,
)
}
?.run {
mutableStateFlow.update { currentState ->
currentState.copy(
viewState = ItemSearchState.ViewState.Error(
message = R.string.internet_connection_required_title
.asText()
.concat(R.string.internet_connection_required_message.asText()),
),
dialogState = null,
)
}
}
}

private fun authenticatorDataPendingReceive(
authenticatorData: DataState.Pending<List<VerificationCodeItem>>,
) {
updateStateWithAuthenticatorData(
authenticatorData = authenticatorData.data,
clearDialogState = false,
)
}

//region Utility Functions
private fun recalculateViewState() {
authenticatorRepository.getLocalVerificationCodesFlow()
.value
.data
?.let { authenticatorData ->
val allItems = authenticatorData +
authenticatorRepository.sharedCodesStateFlow.value.itemsOrEmpty
updateStateWithAuthenticatorData(
authenticatorData = authenticatorData,
clearDialogState = false,
authenticatorData = allItems,
)
}
}

private fun updateStateWithAuthenticatorData(
authenticatorData: List<VerificationCodeItem>,
clearDialogState: Boolean,
) {
mutableStateFlow.update { currentState ->
currentState.copy(
searchTerm = currentState.searchTerm,
viewState = authenticatorData
.filterAndOrganize(state.searchTerm)
.toViewState(searchTerm = state.searchTerm),
dialogState = currentState.dialogState.takeUnless { clearDialogState },
)
}
}
Expand Down Expand Up @@ -255,7 +181,7 @@ class ItemSearchViewModel @Inject constructor(
timeLeftSeconds = timeLeftSeconds,
alertThresholdSeconds = 7,
startIcon = IconData.Local(iconRes = R.drawable.ic_login_item),
label = label,
label = accountName,
)

/**
Expand All @@ -280,7 +206,6 @@ class ItemSearchViewModel @Inject constructor(
data class ItemSearchState(
val searchTerm: String,
val viewState: ViewState,
val dialogState: DialogState?,
) : Parcelable {
/**
* Represents the specific view state for the search screen.
Expand All @@ -300,41 +225,6 @@ data class ItemSearchState(
*/
@Parcelize
data class Empty(val message: Text?) : ViewState()

/**
* Show the error state.
*/
@Parcelize
data class Error(val message: Text) : ViewState()

/**
* Show the loading state.
*/
@Parcelize
data object Loading : ViewState()
}

/**
* Represents the current state of any dialogs on the screen.
*/
sealed class DialogState : Parcelable {

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

/**
* Represents a loading dialog with the given [message].
*/
@Parcelize
data class Loading(
val message: Text,
) : DialogState()
}

/**
Expand Down Expand Up @@ -362,11 +252,6 @@ sealed class ItemSearchAction {
*/
data object BackClick : ItemSearchAction()

/**
* User clicked to dismiss the dialog.
*/
data object DismissDialogClick : ItemSearchAction()

/**
* User updated the search term.
*/
Expand All @@ -386,7 +271,8 @@ sealed class ItemSearchAction {
* Indicates authenticate data was received.
*/
data class AuthenticatorDataReceive(
val dataState: DataState<List<VerificationCodeItem>>,
val localData: DataState<List<VerificationCodeItem>>,
val sharedData: SharedVerificationCodesState,
) : Internal()
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,19 +80,18 @@ fun VaultVerificationCodeItem(
verticalArrangement = Arrangement.SpaceEvenly,
modifier = Modifier.weight(1f),
) {
issuer?.let {
if (!issuer.isNullOrEmpty()) {
Text(
text = it,
text = issuer,
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurface,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}

supportingLabel?.let {
if (!supportingLabel.isNullOrEmpty()) {
Text(
text = it,
text = supportingLabel,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 1,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import com.bitwarden.authenticator.ui.authenticator.feature.search.ItemSearchVie
*/
class SearchHandlers(
val onBackClick: () -> Unit,
val onDismissRequest: () -> Unit,
val onItemClick: (String) -> Unit,
val onSearchTermChange: (String) -> Unit,
) {
Expand All @@ -25,7 +24,6 @@ class SearchHandlers(
fun create(viewModel: ItemSearchViewModel): SearchHandlers =
SearchHandlers(
onBackClick = { viewModel.trySendAction(ItemSearchAction.BackClick) },
onDismissRequest = { viewModel.trySendAction(ItemSearchAction.DismissDialogClick) },
onItemClick = { viewModel.trySendAction(ItemSearchAction.ItemClick(it)) },
onSearchTermChange = {
viewModel.trySendAction(ItemSearchAction.SearchTermChange(it))
Expand Down
Loading

0 comments on commit 78d7865

Please sign in to comment.