BIT-541, 1370 Adding icon loading for login items (#654)

This commit is contained in:
Oleg Semenenko 2024-01-17 21:14:23 -06:00 committed by Álison Fernandes
parent ff7a015472
commit a87bcd28ff
22 changed files with 599 additions and 67 deletions

View file

@ -6,6 +6,7 @@ import java.net.URI
private const val DEFAULT_WEB_VAULT_URL: String = "https://vault.bitwarden.com"
private const val DEFAULT_WEB_SEND_URL: String = "https://send.bitwarden.com/#"
private const val DEFAULT_ICON_URL: String = "https://icons.bitwarden.net/"
/**
* Returns the base web vault URL. This will check for a custom [EnvironmentUrlDataJson.webVault]
@ -37,6 +38,17 @@ val EnvironmentUrlDataJson.baseWebSendUrl: String
?.let { "$it/#/send/" }
?: DEFAULT_WEB_SEND_URL
/**
* Returns a base icon url based on the environment or the default value if values are missing.
*/
val EnvironmentUrlDataJson.baseIconUrl: String
get() =
this
.icon
.takeIf { !it.isNullOrBlank() }
?: base.takeIf { it.isNotBlank() }?.let { "$it/icons" }
?: DEFAULT_ICON_URL
/**
* Returns the appropriate pre-defined labels for environments matching the known US/EU values.
* Otherwise returns the host of the custom base URL.

View file

@ -0,0 +1,50 @@
package com.x8bit.bitwarden.ui.platform.components
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi
import com.bumptech.glide.integration.compose.GlideImage
import com.bumptech.glide.integration.compose.placeholder
import com.x8bit.bitwarden.ui.platform.components.model.IconData
/**
* Represents a Bitwarden icon that is either locally loaded or loaded using glide.
*
* @param iconData Label for the text field.
* @param tint the color to be applied as the tint for the icon.
* @param modifier A [Modifier] for the composable.
* @param contentDescription A description of the switch's UI for accessibility purposes.
*/
@OptIn(ExperimentalGlideComposeApi::class)
@Composable
fun BitwardenIcon(
iconData: IconData,
tint: Color,
modifier: Modifier = Modifier,
contentDescription: String? = null,
) {
when (iconData) {
is IconData.Network -> {
GlideImage(
model = iconData.uri,
failure = placeholder(iconData.fallbackIconRes),
contentDescription = contentDescription,
modifier = modifier,
) {
it.placeholder(iconData.fallbackIconRes)
}
}
is IconData.Local -> {
Icon(
painter = painterResource(id = iconData.iconRes),
contentDescription = contentDescription,
tint = tint,
modifier = modifier,
)
}
}
}

View file

@ -30,6 +30,7 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.components.model.IconData
import com.x8bit.bitwarden.ui.platform.components.model.IconResource
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
import kotlinx.collections.immutable.PersistentList
@ -52,7 +53,7 @@ import kotlinx.collections.immutable.persistentListOf
@Composable
fun BitwardenListItem(
label: String,
startIcon: Painter,
startIcon: IconData,
onClick: () -> Unit,
selectionDataList: PersistentList<SelectionItemData>,
modifier: Modifier = Modifier,
@ -73,8 +74,8 @@ fun BitwardenListItem(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(16.dp),
) {
Icon(
painter = startIcon,
BitwardenIcon(
iconData = startIcon,
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurface,
modifier = Modifier.size(24.dp),
@ -159,7 +160,7 @@ private fun BitwardenListItem_preview() {
BitwardenListItem(
label = "Sample Label",
supportingLabel = "Jan 3, 2024, 10:35 AM",
startIcon = painterResource(id = R.drawable.ic_send_text),
startIcon = IconData.Local(R.drawable.ic_send_text),
onClick = {},
selectionDataList = persistentListOf(),
)

View file

@ -0,0 +1,31 @@
package com.x8bit.bitwarden.ui.platform.components.model
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
/**
* A class to denote the type of icon being passed.
*/
@Parcelize
sealed class IconData : Parcelable {
/**
* Data class representing the resources required for an icon.
*
* @property iconRes the resource for the local icon.
*/
data class Local(
val iconRes: Int,
) : IconData()
/**
* Data class representing the resources required for a network-based icon.
*
* @property uri the link for the icon.
* @property fallbackIconRes fallback resource if the image cannot be loaded.
*/
data class Network(
val uri: String,
val fallbackIconRes: Int,
) : IconData()
}

View file

@ -16,6 +16,7 @@ import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.components.BitwardenGroupItem
import com.x8bit.bitwarden.ui.platform.components.BitwardenListHeaderText
import com.x8bit.bitwarden.ui.platform.components.BitwardenListHeaderTextWithSupportLabel
import com.x8bit.bitwarden.ui.platform.components.model.IconData
import com.x8bit.bitwarden.ui.tools.feature.send.handlers.SendHandlers
/**
@ -75,7 +76,7 @@ fun SendContent(
items(state.sendItems) {
SendListItem(
startIcon = painterResource(id = it.type.iconRes),
startIcon = IconData.Local(it.type.iconRes),
label = it.name,
supportingLabel = it.deletionDate,
trailingLabelIcons = it.iconList,

View file

@ -14,6 +14,7 @@ import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.components.BitwardenListItem
import com.x8bit.bitwarden.ui.platform.components.BitwardenTwoButtonDialog
import com.x8bit.bitwarden.ui.platform.components.SelectionItemData
import com.x8bit.bitwarden.ui.platform.components.model.IconData
import com.x8bit.bitwarden.ui.platform.components.model.IconResource
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
import com.x8bit.bitwarden.ui.platform.util.persistentListOfNotNull
@ -40,7 +41,7 @@ import com.x8bit.bitwarden.ui.tools.feature.send.model.SendStatusIcon
fun SendListItem(
label: String,
supportingLabel: String,
startIcon: Painter,
startIcon: IconData,
trailingLabelIcons: List<SendStatusIcon>,
onClick: () -> Unit,
onEditClick: () -> Unit,
@ -111,7 +112,7 @@ private fun SendListItem_preview() {
SendListItem(
label = "Sample Label",
supportingLabel = "Jan 3, 2024, 10:35 AM",
startIcon = painterResource(id = R.drawable.ic_send_text),
startIcon = IconData.Local(R.drawable.ic_send_text),
trailingLabelIcons = emptyList(),
onClick = {},
onCopyClick = {},

View file

@ -6,7 +6,6 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.x8bit.bitwarden.R
@ -36,7 +35,7 @@ fun VaultItemListingContent(
}
items(state.displayItemList) {
VaultEntryListItem(
startIcon = painterResource(id = it.iconRes),
startIcon = it.iconData,
label = it.title,
supportingLabel = it.subtitle,
onClick = { vaultItemClick(it.id) },

View file

@ -1,16 +1,19 @@
package com.x8bit.bitwarden.ui.vault.feature.itemlisting
import androidx.annotation.DrawableRes
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
import com.x8bit.bitwarden.data.platform.repository.model.DataState
import com.x8bit.bitwarden.data.platform.repository.util.baseIconUrl
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
import com.x8bit.bitwarden.data.vault.repository.model.VaultData
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.IconData
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.util.determineListingPredicate
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.util.toItemListingType
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.util.toViewState
@ -30,16 +33,25 @@ import javax.inject.Inject
class VaultItemListingViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
private val vaultRepository: VaultRepository,
private val environmentRepository: EnvironmentRepository,
private val settingsRepository: SettingsRepository,
) : BaseViewModel<VaultItemListingState, VaultItemListingEvent, VaultItemListingsAction>(
initialState = VaultItemListingState(
itemListingType = VaultItemListingArgs(savedStateHandle = savedStateHandle)
.vaultItemListingType
.toItemListingType(),
viewState = VaultItemListingState.ViewState.Loading,
baseIconUrl = environmentRepository.environment.environmentUrlData.baseIconUrl,
isIconLoadingDisabled = settingsRepository.isIconLoadingDisabled,
),
) {
init {
settingsRepository
.isIconLoadingDisabledFlow
.onEach { sendAction(VaultItemListingsAction.Internal.IconLoadingSettingReceive(it)) }
.launchIn(viewModelScope)
vaultRepository
.vaultDataStateFlow
.onEach { sendAction(VaultItemListingsAction.Internal.VaultDataReceive(it)) }
@ -54,6 +66,8 @@ class VaultItemListingViewModel @Inject constructor(
is VaultItemListingsAction.AddVaultItemClick -> handleAddVaultItemClick()
is VaultItemListingsAction.RefreshClick -> handleRefreshClick()
is VaultItemListingsAction.Internal.VaultDataReceive -> handleVaultDataReceive(action)
is VaultItemListingsAction.Internal.IconLoadingSettingReceive ->
handleIconsSettingReceived(action)
}
}
@ -99,6 +113,18 @@ class VaultItemListingViewModel @Inject constructor(
is DataState.Pending -> vaultPendingReceive(vaultData = vaultData)
}
}
private fun handleIconsSettingReceived(
action: VaultItemListingsAction.Internal.IconLoadingSettingReceive,
) {
mutableStateFlow.update {
it.copy(isIconLoadingDisabled = action.isIconLoadingDisabled)
}
vaultRepository.vaultDataStateFlow.value.data?.let { vaultData ->
updateStateWithVaultData(vaultData)
}
}
//endregion VaultItemListing Handlers
private fun vaultErrorReceive(vaultData: DataState.Error<VaultData>) {
@ -159,7 +185,10 @@ class VaultItemListingViewModel @Inject constructor(
.filter { cipherView ->
cipherView.determineListingPredicate(currentState.itemListingType)
}
.toViewState(),
.toViewState(
baseIconUrl = state.baseIconUrl,
isIconLoadingDisabled = state.isIconLoadingDisabled,
),
)
}
}
@ -171,6 +200,8 @@ class VaultItemListingViewModel @Inject constructor(
data class VaultItemListingState(
val itemListingType: ItemListingType,
val viewState: ViewState,
val baseIconUrl: String,
val isIconLoadingDisabled: Boolean,
) {
/**
@ -214,16 +245,13 @@ data class VaultItemListingState(
* @property id the id of the item.
* @property title title of the item.
* @property subtitle subtitle of the item (nullable).
* @property uri uri for the icon to be displayed (nullable).
* @property iconRes the icon to be displayed.
* @property iconData data for the icon to be displayed (nullable).
*/
data class DisplayItem(
val id: String,
val title: String,
val subtitle: String?,
val uri: String?,
@DrawableRes
val iconRes: Int,
val iconData: IconData,
)
/**
@ -399,6 +427,13 @@ sealed class VaultItemListingsAction {
*/
sealed class Internal : VaultItemListingsAction() {
/**
* Indicates the icon setting was received.
*/
data class IconLoadingSettingReceive(
val isIconLoadingDisabled: Boolean,
) : Internal()
/**
* Indicates vault data was received.
*/

View file

@ -6,7 +6,9 @@ import com.bitwarden.core.CipherView
import com.bitwarden.core.CollectionView
import com.bitwarden.core.FolderView
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.components.model.IconData
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.VaultItemListingState
import com.x8bit.bitwarden.ui.vault.feature.vault.util.toLoginIconData
/**
* Determines a predicate to filter a list of [CipherView] based on the
@ -48,9 +50,17 @@ fun CipherView.determineListingPredicate(
/**
* Transforms a list of [CipherView] into [VaultItemListingState.ViewState].
*/
fun List<CipherView>.toViewState(): VaultItemListingState.ViewState =
fun List<CipherView>.toViewState(
baseIconUrl: String,
isIconLoadingDisabled: Boolean,
): VaultItemListingState.ViewState =
if (isNotEmpty()) {
VaultItemListingState.ViewState.Content(displayItemList = toDisplayItemList())
VaultItemListingState.ViewState.Content(
displayItemList = toDisplayItemList(
baseIconUrl = baseIconUrl,
isIconLoadingDisabled = isIconLoadingDisabled,
),
)
} else {
VaultItemListingState.ViewState.NoItems
}
@ -82,18 +92,49 @@ fun VaultItemListingState.ItemListingType.updateWithAdditionalDataIfNecessary(
is VaultItemListingState.ItemListingType.Trash -> this
}
private fun List<CipherView>.toDisplayItemList(): List<VaultItemListingState.DisplayItem> =
this.map { it.toDisplayItem() }
private fun List<CipherView>.toDisplayItemList(
baseIconUrl: String,
isIconLoadingDisabled: Boolean,
): List<VaultItemListingState.DisplayItem> =
this.map {
it.toDisplayItem(
baseIconUrl = baseIconUrl,
isIconLoadingDisabled = isIconLoadingDisabled,
)
}
private fun CipherView.toDisplayItem(): VaultItemListingState.DisplayItem =
private fun CipherView.toDisplayItem(
baseIconUrl: String,
isIconLoadingDisabled: Boolean,
): VaultItemListingState.DisplayItem =
VaultItemListingState.DisplayItem(
id = id.orEmpty(),
title = name,
subtitle = subtitle,
iconRes = type.iconRes,
uri = uri,
iconData = this.toIconData(
baseIconUrl = baseIconUrl,
isIconLoadingDisabled = isIconLoadingDisabled,
),
)
private fun CipherView.toIconData(
baseIconUrl: String,
isIconLoadingDisabled: Boolean,
): IconData {
return when (this.type) {
CipherType.LOGIN -> {
login?.uris.toLoginIconData(
baseIconUrl = baseIconUrl,
isIconLoadingDisabled = isIconLoadingDisabled,
)
}
else -> {
IconData.Local(iconRes = this.type.iconRes)
}
}
}
@Suppress("MagicNumber")
private val CipherView.subtitle: String?
get() = when (type) {
@ -122,18 +163,3 @@ private val CipherType.iconRes: Int
CipherType.CARD -> R.drawable.ic_card_item
CipherType.IDENTITY -> R.drawable.ic_identity_item
}
private val CipherView.uri: String?
get() = when (type) {
CipherType.LOGIN -> {
login
?.uris
?.firstOrNull()
?.uri
.orEmpty()
}
CipherType.SECURE_NOTE -> null
CipherType.CARD -> null
CipherType.IDENTITY -> null
}

View file

@ -82,7 +82,7 @@ fun VaultContent(
items(state.favoriteItems) { favoriteItem ->
VaultEntryListItem(
startIcon = painterResource(id = favoriteItem.startIcon),
startIcon = favoriteItem.startIcon,
label = favoriteItem.name(),
supportingLabel = favoriteItem.supportingLabel?.invoke(),
onClick = { vaultItemClick(favoriteItem) },
@ -234,8 +234,9 @@ fun VaultContent(
)
}
items(state.noFolderItems) { noFolderItem ->
VaultEntryListItem(
startIcon = painterResource(id = noFolderItem.startIcon),
startIcon = noFolderItem.startIcon,
label = noFolderItem.name(),
supportingLabel = noFolderItem.supportingLabel?.invoke(),
onClick = { vaultItemClick(noFolderItem) },

View file

@ -3,13 +3,12 @@ package com.x8bit.bitwarden.ui.vault.feature.vault
import android.widget.Toast
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.components.BitwardenListItem
import com.x8bit.bitwarden.ui.platform.components.SelectionItemData
import com.x8bit.bitwarden.ui.platform.components.model.IconData
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
import kotlinx.collections.immutable.persistentListOf
@ -24,7 +23,7 @@ import kotlinx.collections.immutable.persistentListOf
*/
@Composable
fun VaultEntryListItem(
startIcon: Painter,
startIcon: IconData,
label: String,
onClick: () -> Unit,
modifier: Modifier = Modifier,
@ -54,7 +53,7 @@ fun VaultEntryListItem(
private fun VaultEntryListItem_preview() {
BitwardenTheme {
VaultEntryListItem(
startIcon = painterResource(id = R.drawable.ic_login_item),
startIcon = IconData.Local(R.drawable.ic_login_item),
label = "Example Login",
supportingLabel = "Username",
onClick = {},

View file

@ -1,7 +1,6 @@
package com.x8bit.bitwarden.ui.vault.feature.vault
import android.os.Parcelable
import androidx.annotation.DrawableRes
import androidx.compose.ui.graphics.Color
import androidx.lifecycle.viewModelScope
import com.x8bit.bitwarden.R
@ -10,6 +9,7 @@ import com.x8bit.bitwarden.data.auth.repository.model.SwitchAccountResult
import com.x8bit.bitwarden.data.auth.repository.model.UserState
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
import com.x8bit.bitwarden.data.platform.repository.model.DataState
import com.x8bit.bitwarden.data.platform.repository.util.baseIconUrl
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
import com.x8bit.bitwarden.data.vault.repository.model.VaultData
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
@ -18,6 +18,7 @@ import com.x8bit.bitwarden.ui.platform.base.util.asText
import com.x8bit.bitwarden.ui.platform.base.util.concat
import com.x8bit.bitwarden.ui.platform.base.util.hexToColor
import com.x8bit.bitwarden.ui.platform.components.model.AccountSummary
import com.x8bit.bitwarden.ui.platform.components.model.IconData
import com.x8bit.bitwarden.ui.vault.feature.vault.model.VaultFilterData
import com.x8bit.bitwarden.ui.vault.feature.vault.model.VaultFilterType
import com.x8bit.bitwarden.ui.vault.feature.vault.util.initials
@ -43,8 +44,8 @@ import javax.inject.Inject
@HiltViewModel
class VaultViewModel @Inject constructor(
private val authRepository: AuthRepository,
private val settingsRepository: SettingsRepository,
private val vaultRepository: VaultRepository,
private val settingsRepository: SettingsRepository,
) : BaseViewModel<VaultState, VaultEvent, VaultAction>(
initialState = run {
val userState = requireNotNull(authRepository.userStateFlow.value)
@ -59,8 +60,10 @@ class VaultViewModel @Inject constructor(
accountSummaries = accountSummaries,
vaultFilterData = vaultFilterData,
viewState = VaultState.ViewState.Loading,
isIconLoadingDisabled = settingsRepository.isIconLoadingDisabled,
isPremium = userState.activeAccount.isPremium,
isPullToRefreshSettingEnabled = settingsRepository.getPullToRefreshEnabledFlow().value,
baseIconUrl = userState.activeAccount.environment.environmentUrlData.baseIconUrl,
)
},
) {
@ -77,6 +80,12 @@ class VaultViewModel @Inject constructor(
.onEach(::sendAction)
.launchIn(viewModelScope)
settingsRepository
.isIconLoadingDisabledFlow
.map { VaultAction.Internal.IconLoadingSettingReceive(it) }
.onEach(::sendAction)
.launchIn(viewModelScope)
vaultRepository
.vaultDataStateFlow
.onEach { sendAction(VaultAction.Internal.VaultDataReceive(vaultData = it)) }
@ -118,6 +127,16 @@ class VaultViewModel @Inject constructor(
}
}
private fun handleIconLoadingSettingReceive(
action: VaultAction.Internal.IconLoadingSettingReceive,
) {
mutableStateFlow.update {
it.copy(isIconLoadingDisabled = action.isIconLoadingDisabled)
}
updateViewState(vaultRepository.vaultDataStateFlow.value)
}
//region VaultAction Handlers
private fun handleAddItemClick() {
sendEvent(VaultEvent.NavigateToAddItemScreen)
@ -252,6 +271,9 @@ class VaultViewModel @Inject constructor(
is VaultAction.Internal.UserStateUpdateReceive -> handleUserStateUpdateReceive(action)
is VaultAction.Internal.VaultDataReceive -> handleVaultDataReceive(action)
is VaultAction.Internal.IconLoadingSettingReceive -> handleIconLoadingSettingReceive(
action,
)
}
}
@ -308,8 +330,10 @@ class VaultViewModel @Inject constructor(
private fun vaultErrorReceive(vaultData: DataState.Error<VaultData>) {
mutableStateFlow.updateToErrorStateOrDialog(
baseIconUrl = state.baseIconUrl,
vaultData = vaultData.data,
vaultFilterType = vaultFilterTypeOrDefault,
isIconLoadingDisabled = state.isIconLoadingDisabled,
isPremium = state.isPremium,
errorTitle = R.string.an_error_has_occurred.asText(),
errorMessage = R.string.generic_error_message.asText(),
@ -328,6 +352,8 @@ class VaultViewModel @Inject constructor(
mutableStateFlow.update {
it.copy(
viewState = vaultData.data.toViewState(
baseIconUrl = state.baseIconUrl,
isIconLoadingDisabled = state.isIconLoadingDisabled,
isPremium = state.isPremium,
vaultFilterType = vaultFilterTypeOrDefault,
),
@ -343,10 +369,12 @@ class VaultViewModel @Inject constructor(
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,
errorMessage = R.string.internet_connection_required_message.asText(),
)
sendEvent(VaultEvent.DismissPullToRefresh)
@ -357,6 +385,8 @@ class VaultViewModel @Inject constructor(
mutableStateFlow.update {
it.copy(
viewState = vaultData.data.toViewState(
baseIconUrl = state.baseIconUrl,
isIconLoadingDisabled = state.isIconLoadingDisabled,
isPremium = state.isPremium,
vaultFilterType = vaultFilterTypeOrDefault,
),
@ -391,6 +421,8 @@ data class VaultState(
val isSwitchingAccounts: Boolean = false,
val isPremium: Boolean,
private val isPullToRefreshSettingEnabled: Boolean,
val baseIconUrl: String,
val isIconLoadingDisabled: Boolean,
) : Parcelable {
/**
@ -536,8 +568,7 @@ data class VaultState(
*/
abstract val name: Text
@get:DrawableRes
abstract val startIcon: Int
abstract val startIcon: IconData
/**
* An optional supporting label for the vault item that provides additional information.
@ -555,9 +586,9 @@ data class VaultState(
data class Login(
override val id: String,
override val name: Text,
override val startIcon: IconData = IconData.Local(R.drawable.ic_login_item),
val username: Text?,
) : VaultItem() {
override val startIcon: Int get() = R.drawable.ic_login_item
override val supportingLabel: Text? get() = username
}
@ -571,10 +602,10 @@ data class VaultState(
data class Card(
override val id: String,
override val name: Text,
override val startIcon: IconData = IconData.Local(R.drawable.ic_card_item),
val brand: Text? = null,
val lastFourDigits: Text? = null,
) : VaultItem() {
override val startIcon: Int get() = R.drawable.ic_card_item
override val supportingLabel: Text?
get() = when {
brand != null && lastFourDigits != null -> brand
@ -597,9 +628,9 @@ data class VaultState(
data class Identity(
override val id: String,
override val name: Text,
override val startIcon: IconData = IconData.Local(R.drawable.ic_identity_item),
val firstName: Text?,
) : VaultItem() {
override val startIcon: Int get() = R.drawable.ic_identity_item
override val supportingLabel: Text? get() = firstName
}
@ -611,8 +642,8 @@ data class VaultState(
data class SecureNote(
override val id: String,
override val name: Text,
override val startIcon: IconData = IconData.Local(R.drawable.ic_secure_note_item),
) : VaultItem() {
override val startIcon: Int get() = R.drawable.ic_secure_note_item
override val supportingLabel: Text? get() = null
}
}
@ -841,6 +872,13 @@ sealed class VaultAction {
*/
sealed class Internal : VaultAction() {
/**
* Indicates that the icon loading setting has been changed.
*/
data class IconLoadingSettingReceive(
val isIconLoadingDisabled: Boolean,
) : Internal()
/**
* Indicates that the pull to refresh feature toggle has changed.
*/
@ -862,9 +900,12 @@ sealed class VaultAction {
}
}
@Suppress("LongParameterList")
private fun MutableStateFlow<VaultState>.updateToErrorStateOrDialog(
baseIconUrl: String,
vaultData: VaultData?,
vaultFilterType: VaultFilterType,
isIconLoadingDisabled: Boolean,
isPremium: Boolean,
errorTitle: Text,
errorMessage: Text,
@ -873,8 +914,10 @@ private fun MutableStateFlow<VaultState>.updateToErrorStateOrDialog(
if (vaultData != null) {
it.copy(
viewState = vaultData.toViewState(
baseIconUrl = baseIconUrl,
isPremium = isPremium,
vaultFilterType = vaultFilterType,
isIconLoadingDisabled = isIconLoadingDisabled,
),
dialog = VaultState.DialogState.Error(
title = errorTitle,

View file

@ -1,19 +1,28 @@
package com.x8bit.bitwarden.ui.vault.feature.vault.util
import android.net.Uri
import com.bitwarden.core.CipherType
import com.bitwarden.core.CipherView
import com.bitwarden.core.CollectionView
import com.bitwarden.core.FolderView
import com.bitwarden.core.LoginUriView
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.vault.repository.model.VaultData
import com.x8bit.bitwarden.ui.platform.base.util.asText
import com.x8bit.bitwarden.ui.platform.components.model.IconData
import com.x8bit.bitwarden.ui.vault.feature.vault.VaultState
import com.x8bit.bitwarden.ui.vault.feature.vault.model.VaultFilterType
private const val ANDROID_URI = "androidapp://"
private const val IOS_URI = "iosapp://"
/**
* Transforms [VaultData] into [VaultState.ViewState] using the given [vaultFilterType].
*/
fun VaultData.toViewState(
isPremium: Boolean,
isIconLoadingDisabled: Boolean,
baseIconUrl: String,
vaultFilterType: VaultFilterType,
): VaultState.ViewState {
val filteredCipherViewList = cipherViewList.toFilteredList(vaultFilterType)
@ -36,7 +45,12 @@ fun VaultData.toViewState(
.count { it.type == CipherType.SECURE_NOTE },
favoriteItems = filteredCipherViewList
.filter { it.favorite }
.mapNotNull { it.toVaultItemOrNull() },
.mapNotNull {
it.toVaultItemOrNull(
isIconLoadingDisabled = isIconLoadingDisabled,
baseIconUrl = baseIconUrl,
)
},
folderItems = filteredFolderViewList.map { folderView ->
VaultState.ViewState.FolderItem(
id = folderView.id,
@ -47,7 +61,12 @@ fun VaultData.toViewState(
},
noFolderItems = filteredCipherViewList
.filter { it.folderId.isNullOrBlank() }
.mapNotNull { it.toVaultItemOrNull() },
.mapNotNull {
it.toVaultItemOrNull(
isIconLoadingDisabled = isIconLoadingDisabled,
baseIconUrl = baseIconUrl,
)
},
collectionItems = filteredCollectionViewList
.filter { it.id != null }
.map { collectionView ->
@ -67,17 +86,66 @@ fun VaultData.toViewState(
}
}
/**
* Method to build the icon data for login item icons.
*/
@Suppress("ReturnCount")
fun List<LoginUriView>?.toLoginIconData(
isIconLoadingDisabled: Boolean,
baseIconUrl: String,
): IconData {
val localIconData = IconData.Local(R.drawable.ic_login_item)
var uri = this
?.map { it.uri }
?.firstOrNull { uri -> uri?.contains(".") == true }
?: return localIconData
if (uri.startsWith(ANDROID_URI)) {
return IconData.Local(R.drawable.ic_settings)
}
if (uri.startsWith(IOS_URI)) {
return IconData.Local(R.drawable.ic_settings)
}
if (isIconLoadingDisabled) {
return localIconData
}
if (!uri.contains("://")) {
uri = "http://$uri"
}
val iconUri = Uri.parse(uri)
val hostname = iconUri.host
val url = "$baseIconUrl/$hostname/icon.png"
return IconData.Network(
uri = url,
fallbackIconRes = R.drawable.ic_login_item,
)
}
/**
* Transforms a [CipherView] into a [VaultState.ViewState.VaultItem].
*/
@Suppress("MagicNumber")
private fun CipherView.toVaultItemOrNull(): VaultState.ViewState.VaultItem? {
private fun CipherView.toVaultItemOrNull(
isIconLoadingDisabled: Boolean,
baseIconUrl: String,
): VaultState.ViewState.VaultItem? {
val id = this.id ?: return null
return when (type) {
CipherType.LOGIN -> VaultState.ViewState.VaultItem.Login(
id = id,
name = name.asText(),
username = login?.username?.asText(),
startIcon = login?.uris.toLoginIconData(
isIconLoadingDisabled = isIconLoadingDisabled,
baseIconUrl = baseIconUrl,
),
)
CipherType.SECURE_NOTE -> VaultState.ViewState.VaultItem.SecureNote(

View file

@ -170,6 +170,39 @@ class EnvironmentUrlsDataJsonExtensionsTest {
(null as EnvironmentUrlDataJson?).toEnvironmentUrlsOrDefault(),
)
}
@Test
fun `toIconBaseurl should return icon if value is present`() {
assertEquals(
DEFAULT_CUSTOM_ENVIRONMENT_URL_DATA.baseIconUrl,
"icon",
)
}
@Test
fun `toIconBaseurl should return base value if icon is null`() {
assertEquals(
DEFAULT_CUSTOM_ENVIRONMENT_URL_DATA
.copy(icon = null)
.baseIconUrl,
"base/icons",
)
}
@Test
fun `toIconBaseurl should return default url if base is empty and icon is null`() {
val expectedUrl = "https://icons.bitwarden.net/"
assertEquals(
expectedUrl,
DEFAULT_CUSTOM_ENVIRONMENT_URL_DATA
.copy(
base = "",
icon = null,
)
.baseIconUrl,
)
}
}
private val DEFAULT_CUSTOM_ENVIRONMENT_URL_DATA = EnvironmentUrlDataJson(

View file

@ -85,7 +85,7 @@ fun createMockLoginView(number: Int): LoginView =
*/
fun createMockUriView(number: Int): LoginUriView =
LoginUriView(
uri = "mockUri-$number",
uri = "www.mockuri$number.com",
match = UriMatchType.HOST,
)

View file

@ -10,9 +10,12 @@ import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performScrollToNode
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.platform.repository.model.Environment
import com.x8bit.bitwarden.data.platform.repository.util.baseIconUrl
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
import com.x8bit.bitwarden.ui.platform.base.util.asText
import com.x8bit.bitwarden.ui.platform.components.model.IconData
import com.x8bit.bitwarden.ui.util.isProgressBar
import io.mockk.every
import io.mockk.mockk
@ -393,6 +396,8 @@ class VaultItemListingScreenTest : BaseComposeTest() {
private val DEFAULT_STATE = VaultItemListingState(
itemListingType = VaultItemListingState.ItemListingType.Login,
viewState = VaultItemListingState.ViewState.Loading,
isIconLoadingDisabled = false,
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
)
private fun createDisplayItem(number: Int): VaultItemListingState.DisplayItem =
@ -400,6 +405,5 @@ private fun createDisplayItem(number: Int): VaultItemListingState.DisplayItem =
id = "mockId-$number",
title = "mockTitle-$number",
subtitle = "mockSubtitle-$number",
uri = "mockUri-$number",
iconRes = R.drawable.ic_card_item,
iconData = IconData.Local(R.drawable.ic_card_item),
)

View file

@ -1,9 +1,14 @@
package com.x8bit.bitwarden.ui.vault.feature.itemlisting
import android.net.Uri
import androidx.lifecycle.SavedStateHandle
import app.cash.turbine.test
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
import com.x8bit.bitwarden.data.platform.repository.model.DataState
import com.x8bit.bitwarden.data.platform.repository.model.Environment
import com.x8bit.bitwarden.data.platform.repository.util.baseIconUrl
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCipherView
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCollectionView
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockFolderView
@ -17,10 +22,14 @@ import com.x8bit.bitwarden.ui.vault.feature.itemlisting.util.createMockItemListi
import com.x8bit.bitwarden.ui.vault.model.VaultItemListingType
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.unmockkStatic
import io.mockk.verify
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
class VaultItemListingViewModelTest : BaseViewModelTest() {
@ -31,6 +40,16 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
every { vaultDataStateFlow } returns mutableVaultDataStateFlow
every { sync() } returns Unit
}
private val environmentRepository: EnvironmentRepository = mockk {
every { environment } returns Environment.Us
every { environmentStateFlow } returns mockk()
}
private val mutableIsIconLoadingDisabledFlow = MutableStateFlow(false)
private val settingsRepository: SettingsRepository = mockk() {
every { isIconLoadingDisabled } returns false
every { isIconLoadingDisabledFlow } returns mutableIsIconLoadingDisabledFlow
}
private val initialState = createVaultItemListingState()
private val initialSavedStateHandle = createSavedStateHandleWithVaultItemListingType(
vaultItemListingType = VaultItemListingType.Login,
@ -92,6 +111,8 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
@Test
fun `vaultDataStateFlow Loaded with items should update ViewState to Content`() =
runTest {
setupMockUri()
mutableVaultDataStateFlow.tryEmit(
value = DataState.Loaded(
data = VaultData(
@ -178,6 +199,8 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
@Test
fun `vaultDataStateFlow Pending with data should update state to Content`() = runTest {
setupMockUri()
mutableVaultDataStateFlow.tryEmit(
value = DataState.Pending(
data = VaultData(
@ -267,6 +290,8 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
@Test
fun `vaultDataStateFlow Error with data should update state to Content`() = runTest {
setupMockUri()
mutableVaultDataStateFlow.tryEmit(
value = DataState.Error(
data = VaultData(
@ -291,6 +316,8 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
),
viewModel.stateFlow.value,
)
unmockkStatic(Uri::class)
}
@Test
@ -363,6 +390,8 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
@Test
fun `vaultDataStateFlow NoNetwork with data should update state to Content`() = runTest {
setupMockUri()
mutableVaultDataStateFlow.tryEmit(
value = DataState.NoNetwork(
data = VaultData(
@ -386,6 +415,8 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
),
viewModel.stateFlow.value,
)
unmockkStatic(Uri::class)
}
@Test
@ -434,6 +465,16 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
)
}
@Test
fun `icon loading state updates should update isIconLoadingDisabled`() = runTest {
val viewModel = createVaultItemListingViewModel()
assertFalse(viewModel.stateFlow.value.isIconLoadingDisabled)
mutableIsIconLoadingDisabledFlow.value = true
assertTrue(viewModel.stateFlow.value.isIconLoadingDisabled)
}
@Suppress("CyclomaticComplexMethod")
private fun createSavedStateHandleWithVaultItemListingType(
vaultItemListingType: VaultItemListingType,
@ -464,6 +505,13 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
)
}
private fun setupMockUri() {
mockkStatic(Uri::class)
val uriMock = mockk<Uri>()
every { Uri.parse(any()) } returns uriMock
every { uriMock.host } returns "www.mockuri.com"
}
private fun createVaultItemListingViewModel(
savedStateHandle: SavedStateHandle = initialSavedStateHandle,
vaultRepository: VaultRepository = this.vaultRepository,
@ -471,6 +519,8 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
VaultItemListingViewModel(
savedStateHandle = savedStateHandle,
vaultRepository = vaultRepository,
environmentRepository = environmentRepository,
settingsRepository = settingsRepository,
)
@Suppress("MaxLineLength")
@ -481,5 +531,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
VaultItemListingState(
itemListingType = itemListingType,
viewState = viewState,
baseIconUrl = environmentRepository.environment.environmentUrlData.baseIconUrl,
isIconLoadingDisabled = settingsRepository.isIconLoadingDisabled,
)
}

View file

@ -1,10 +1,17 @@
package com.x8bit.bitwarden.ui.vault.feature.itemlisting.util
import android.net.Uri
import com.bitwarden.core.CipherType
import com.x8bit.bitwarden.data.platform.repository.model.Environment
import com.x8bit.bitwarden.data.platform.repository.util.baseIconUrl
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCipherView
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCollectionView
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockFolderView
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.VaultItemListingState
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.unmockkStatic
import org.junit.Assert.assertEquals
import org.junit.Test
@ -244,6 +251,11 @@ class VaultItemListingDataExtensionsTest {
@Test
fun `toViewState should transform a list of CipherViews into a ViewState`() {
mockkStatic(Uri::class)
val uriMock = mockk<Uri>()
every { Uri.parse(any()) } returns uriMock
every { uriMock.host } returns "www.mockuri.com"
val cipherViewList = listOf(
createMockCipherView(
number = 1,
@ -267,7 +279,10 @@ class VaultItemListingDataExtensionsTest {
),
)
val result = cipherViewList.toViewState()
val result = cipherViewList.toViewState(
isIconLoadingDisabled = false,
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
)
assertEquals(
VaultItemListingState.ViewState.Content(
@ -292,6 +307,8 @@ class VaultItemListingDataExtensionsTest {
),
result,
)
unmockkStatic(Uri::class)
}
@Test

View file

@ -2,6 +2,7 @@ package com.x8bit.bitwarden.ui.vault.feature.itemlisting.util
import com.bitwarden.core.CipherType
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.components.model.IconData
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.VaultItemListingState
/**
@ -17,8 +18,10 @@ fun createMockItemListingDisplayItem(
id = "mockId-$number",
title = "mockName-$number",
subtitle = "mockUsername-$number",
iconRes = R.drawable.ic_login_item,
uri = "mockUri-$number",
iconData = IconData.Network(
"https://vault.bitwarden.com/icons/www.mockuri.com/icon.png",
fallbackIconRes = R.drawable.ic_login_item,
),
)
}
@ -27,8 +30,7 @@ fun createMockItemListingDisplayItem(
id = "mockId-$number",
title = "mockName-$number",
subtitle = null,
iconRes = R.drawable.ic_secure_note_item,
uri = null,
iconData = IconData.Local(R.drawable.ic_secure_note_item),
)
}
@ -37,8 +39,7 @@ fun createMockItemListingDisplayItem(
id = "mockId-$number",
title = "mockName-$number",
subtitle = "er-$number",
iconRes = R.drawable.ic_card_item,
uri = null,
iconData = IconData.Local(R.drawable.ic_card_item),
)
}
@ -47,8 +48,7 @@ fun createMockItemListingDisplayItem(
id = "mockId-$number",
title = "mockName-$number",
subtitle = "mockFirstName-${number}mockLastName-$number",
iconRes = R.drawable.ic_identity_item,
uri = null,
iconData = IconData.Local(R.drawable.ic_identity_item),
)
}
}

View file

@ -16,6 +16,8 @@ import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performScrollToNode
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.platform.repository.model.Environment
import com.x8bit.bitwarden.data.platform.repository.util.baseIconUrl
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
import com.x8bit.bitwarden.ui.platform.base.util.asText
@ -1071,6 +1073,8 @@ private val DEFAULT_STATE: VaultState = VaultState(
viewState = VaultState.ViewState.Loading,
isPremium = false,
isPullToRefreshSettingEnabled = false,
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
isIconLoadingDisabled = false,
)
private val DEFAULT_CONTENT_VIEW_STATE: VaultState.ViewState.Content = VaultState.ViewState.Content(

View file

@ -10,6 +10,7 @@ import com.x8bit.bitwarden.data.auth.repository.model.UserState.SpecialCircumsta
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
import com.x8bit.bitwarden.data.platform.repository.model.DataState
import com.x8bit.bitwarden.data.platform.repository.model.Environment
import com.x8bit.bitwarden.data.platform.repository.util.baseIconUrl
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCipherView
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCollectionView
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockFolderView
@ -31,12 +32,15 @@ import io.mockk.verify
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
@Suppress("LargeClass")
class VaultViewModelTest : BaseViewModelTest() {
private val mutablePullToRefreshEnabledFlow = MutableStateFlow(false)
private val mutableIsIconLoadingDisabledFlow = MutableStateFlow(false)
private val mutableUserStateFlow =
MutableStateFlow<UserState?>(DEFAULT_USER_STATE)
@ -57,6 +61,8 @@ class VaultViewModelTest : BaseViewModelTest() {
private val settingsRepository: SettingsRepository = mockk {
every { getPullToRefreshEnabledFlow() } returns mutablePullToRefreshEnabledFlow
every { isIconLoadingDisabledFlow } returns mutableIsIconLoadingDisabledFlow
every { isIconLoadingDisabled } returns false
}
private val vaultRepository: VaultRepository =
@ -357,6 +363,8 @@ class VaultViewModelTest : BaseViewModelTest() {
viewState = vaultData.toViewState(
isPremium = true,
vaultFilterType = VaultFilterType.AllVaults,
isIconLoadingDisabled = viewModel.stateFlow.value.isIconLoadingDisabled,
baseIconUrl = viewModel.stateFlow.value.baseIconUrl,
),
)
.copy(
@ -378,6 +386,8 @@ class VaultViewModelTest : BaseViewModelTest() {
viewState = vaultData.toViewState(
isPremium = true,
vaultFilterType = VaultFilterType.MyVault,
isIconLoadingDisabled = viewModel.stateFlow.value.isIconLoadingDisabled,
baseIconUrl = viewModel.stateFlow.value.baseIconUrl,
),
),
viewModel.stateFlow.value,
@ -1032,6 +1042,16 @@ class VaultViewModelTest : BaseViewModelTest() {
)
}
@Test
fun `icon loading state updates should update isIconLoadingDisabled`() {
val viewModel = createViewModel()
assertFalse(viewModel.stateFlow.value.isIconLoadingDisabled)
mutableIsIconLoadingDisabledFlow.value = true
assertTrue(viewModel.stateFlow.value.isIconLoadingDisabled)
}
private fun createViewModel(): VaultViewModel =
VaultViewModel(
authRepository = authRepository,
@ -1120,4 +1140,6 @@ private fun createMockVaultState(
isSwitchingAccounts = false,
isPremium = true,
isPullToRefreshSettingEnabled = false,
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
isIconLoadingDisabled = false,
)

View file

@ -1,13 +1,25 @@
package com.x8bit.bitwarden.ui.vault.feature.vault.util
import android.net.Uri
import com.bitwarden.core.CipherType
import com.bitwarden.core.LoginUriView
import com.bitwarden.core.UriMatchType
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.platform.repository.model.Environment
import com.x8bit.bitwarden.data.platform.repository.util.baseIconUrl
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCipherView
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCollectionView
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockFolderView
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSendView
import com.x8bit.bitwarden.data.vault.repository.model.VaultData
import com.x8bit.bitwarden.ui.platform.base.util.asText
import com.x8bit.bitwarden.ui.platform.components.model.IconData
import com.x8bit.bitwarden.ui.vault.feature.vault.VaultState
import com.x8bit.bitwarden.ui.vault.feature.vault.model.VaultFilterType
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.unmockkStatic
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
@ -25,6 +37,8 @@ class VaultDataExtensionsTest {
val actual = vaultData.toViewState(
isPremium = true,
isIconLoadingDisabled = false,
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
vaultFilterType = VaultFilterType.AllVaults,
)
@ -72,6 +86,8 @@ class VaultDataExtensionsTest {
val actual = vaultData.toViewState(
isPremium = true,
isIconLoadingDisabled = false,
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
vaultFilterType = VaultFilterType.MyVault,
)
@ -116,6 +132,8 @@ class VaultDataExtensionsTest {
val actual = vaultData.toViewState(
isPremium = true,
isIconLoadingDisabled = false,
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
vaultFilterType = VaultFilterType.OrganizationVault(
organizationId = "mockOrganizationId-1",
organizationName = "Mock Organization 1",
@ -156,6 +174,8 @@ class VaultDataExtensionsTest {
val actual = vaultData.toViewState(
isPremium = true,
isIconLoadingDisabled = false,
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
vaultFilterType = VaultFilterType.AllVaults,
)
@ -176,6 +196,8 @@ class VaultDataExtensionsTest {
val actual = vaultData.toViewState(
isPremium = true,
isIconLoadingDisabled = false,
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
vaultFilterType = VaultFilterType.AllVaults,
)
@ -197,6 +219,8 @@ class VaultDataExtensionsTest {
val actual = vaultData.toViewState(
isPremium = true,
isIconLoadingDisabled = false,
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
vaultFilterType = VaultFilterType.AllVaults,
)
@ -230,6 +254,8 @@ class VaultDataExtensionsTest {
val actual = vaultData.toViewState(
isPremium = false,
vaultFilterType = VaultFilterType.AllVaults,
isIconLoadingDisabled = false,
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
)
assertEquals(
@ -248,4 +274,111 @@ class VaultDataExtensionsTest {
actual,
)
}
@Suppress("MaxLineLength")
@Test
fun `toLoginIconData should return a IconData Local type if isIconLoadingDisabled is true`() {
val actual =
createMockCipherView(
number = 1,
cipherType = CipherType.LOGIN,
)
.login
?.uris
.toLoginIconData(
isIconLoadingDisabled = true,
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
)
val expected = IconData.Local(iconRes = R.drawable.ic_login_item)
assertEquals(expected, actual)
}
@Suppress("MaxLineLength")
@Test
fun `toLoginIconData should return a IconData Local type if no valid uris are found`() {
val actual = listOf(
LoginUriView(
uri = "",
match = UriMatchType.HOST,
),
)
.toLoginIconData(
isIconLoadingDisabled = false,
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
)
val expected = IconData.Local(iconRes = R.drawable.ic_login_item)
assertEquals(expected, actual)
}
@Suppress("MaxLineLength")
@Test
fun `toLoginIconData should return a IconData Local type if an Android uri is detected`() {
val actual = listOf(
LoginUriView(
uri = "androidapp://test.com",
match = UriMatchType.HOST,
),
)
.toLoginIconData(
isIconLoadingDisabled = false,
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
)
val expected = IconData.Local(iconRes = R.drawable.ic_settings)
assertEquals(expected, actual)
}
@Suppress("MaxLineLength")
@Test
fun `toLoginIconData should return a IconData Local type if an iOS uri is detected`() {
val actual = listOf(
LoginUriView(
uri = "iosapp://test.com",
match = UriMatchType.HOST,
),
)
.toLoginIconData(
isIconLoadingDisabled = false,
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
)
val expected = IconData.Local(iconRes = R.drawable.ic_settings)
assertEquals(expected, actual)
}
@Suppress("MaxLineLength")
@Test
fun `toLoginIconData should return IconData Network type if isIconLoadingDisabled is false`() {
mockkStatic(Uri::class)
val uriMock = mockk<Uri>()
every { Uri.parse(any()) } returns uriMock
every { uriMock.host } returns "www.mockuri1.com"
val actual =
createMockCipherView(
number = 1,
cipherType = CipherType.LOGIN,
)
.login
?.uris
.toLoginIconData(
isIconLoadingDisabled = false,
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
)
val expected = IconData.Network(
uri = "https://vault.bitwarden.com/icons/www.mockuri1.com/icon.png",
fallbackIconRes = R.drawable.ic_login_item,
)
assertEquals(expected, actual)
unmockkStatic(Uri::class)
}
}