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_VAULT_URL: String = "https://vault.bitwarden.com"
private const val DEFAULT_WEB_SEND_URL: String = "https://send.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] * 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/" } ?.let { "$it/#/send/" }
?: DEFAULT_WEB_SEND_URL ?: 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. * Returns the appropriate pre-defined labels for environments matching the known US/EU values.
* Otherwise returns the host of the custom base URL. * 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.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.x8bit.bitwarden.R 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.components.model.IconResource
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.PersistentList
@ -52,7 +53,7 @@ import kotlinx.collections.immutable.persistentListOf
@Composable @Composable
fun BitwardenListItem( fun BitwardenListItem(
label: String, label: String,
startIcon: Painter, startIcon: IconData,
onClick: () -> Unit, onClick: () -> Unit,
selectionDataList: PersistentList<SelectionItemData>, selectionDataList: PersistentList<SelectionItemData>,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
@ -73,8 +74,8 @@ fun BitwardenListItem(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(16.dp), horizontalArrangement = Arrangement.spacedBy(16.dp),
) { ) {
Icon( BitwardenIcon(
painter = startIcon, iconData = startIcon,
contentDescription = null, contentDescription = null,
tint = MaterialTheme.colorScheme.onSurface, tint = MaterialTheme.colorScheme.onSurface,
modifier = Modifier.size(24.dp), modifier = Modifier.size(24.dp),
@ -159,7 +160,7 @@ private fun BitwardenListItem_preview() {
BitwardenListItem( BitwardenListItem(
label = "Sample Label", label = "Sample Label",
supportingLabel = "Jan 3, 2024, 10:35 AM", supportingLabel = "Jan 3, 2024, 10:35 AM",
startIcon = painterResource(id = R.drawable.ic_send_text), startIcon = IconData.Local(R.drawable.ic_send_text),
onClick = {}, onClick = {},
selectionDataList = persistentListOf(), 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.BitwardenGroupItem
import com.x8bit.bitwarden.ui.platform.components.BitwardenListHeaderText import com.x8bit.bitwarden.ui.platform.components.BitwardenListHeaderText
import com.x8bit.bitwarden.ui.platform.components.BitwardenListHeaderTextWithSupportLabel 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 import com.x8bit.bitwarden.ui.tools.feature.send.handlers.SendHandlers
/** /**
@ -75,7 +76,7 @@ fun SendContent(
items(state.sendItems) { items(state.sendItems) {
SendListItem( SendListItem(
startIcon = painterResource(id = it.type.iconRes), startIcon = IconData.Local(it.type.iconRes),
label = it.name, label = it.name,
supportingLabel = it.deletionDate, supportingLabel = it.deletionDate,
trailingLabelIcons = it.iconList, 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.BitwardenListItem
import com.x8bit.bitwarden.ui.platform.components.BitwardenTwoButtonDialog import com.x8bit.bitwarden.ui.platform.components.BitwardenTwoButtonDialog
import com.x8bit.bitwarden.ui.platform.components.SelectionItemData 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.components.model.IconResource
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
import com.x8bit.bitwarden.ui.platform.util.persistentListOfNotNull import com.x8bit.bitwarden.ui.platform.util.persistentListOfNotNull
@ -40,7 +41,7 @@ import com.x8bit.bitwarden.ui.tools.feature.send.model.SendStatusIcon
fun SendListItem( fun SendListItem(
label: String, label: String,
supportingLabel: String, supportingLabel: String,
startIcon: Painter, startIcon: IconData,
trailingLabelIcons: List<SendStatusIcon>, trailingLabelIcons: List<SendStatusIcon>,
onClick: () -> Unit, onClick: () -> Unit,
onEditClick: () -> Unit, onEditClick: () -> Unit,
@ -111,7 +112,7 @@ private fun SendListItem_preview() {
SendListItem( SendListItem(
label = "Sample Label", label = "Sample Label",
supportingLabel = "Jan 3, 2024, 10:35 AM", 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(), trailingLabelIcons = emptyList(),
onClick = {}, onClick = {},
onCopyClick = {}, onCopyClick = {},

View file

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

View file

@ -1,16 +1,19 @@
package com.x8bit.bitwarden.ui.vault.feature.itemlisting package com.x8bit.bitwarden.ui.vault.feature.itemlisting
import androidx.annotation.DrawableRes
import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.x8bit.bitwarden.R 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.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.VaultRepository
import com.x8bit.bitwarden.data.vault.repository.model.VaultData import com.x8bit.bitwarden.data.vault.repository.model.VaultData
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel 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.Text
import com.x8bit.bitwarden.ui.platform.base.util.asText 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.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.determineListingPredicate
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.util.toItemListingType import com.x8bit.bitwarden.ui.vault.feature.itemlisting.util.toItemListingType
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.util.toViewState import com.x8bit.bitwarden.ui.vault.feature.itemlisting.util.toViewState
@ -30,16 +33,25 @@ import javax.inject.Inject
class VaultItemListingViewModel @Inject constructor( class VaultItemListingViewModel @Inject constructor(
savedStateHandle: SavedStateHandle, savedStateHandle: SavedStateHandle,
private val vaultRepository: VaultRepository, private val vaultRepository: VaultRepository,
private val environmentRepository: EnvironmentRepository,
private val settingsRepository: SettingsRepository,
) : BaseViewModel<VaultItemListingState, VaultItemListingEvent, VaultItemListingsAction>( ) : BaseViewModel<VaultItemListingState, VaultItemListingEvent, VaultItemListingsAction>(
initialState = VaultItemListingState( initialState = VaultItemListingState(
itemListingType = VaultItemListingArgs(savedStateHandle = savedStateHandle) itemListingType = VaultItemListingArgs(savedStateHandle = savedStateHandle)
.vaultItemListingType .vaultItemListingType
.toItemListingType(), .toItemListingType(),
viewState = VaultItemListingState.ViewState.Loading, viewState = VaultItemListingState.ViewState.Loading,
baseIconUrl = environmentRepository.environment.environmentUrlData.baseIconUrl,
isIconLoadingDisabled = settingsRepository.isIconLoadingDisabled,
), ),
) { ) {
init { init {
settingsRepository
.isIconLoadingDisabledFlow
.onEach { sendAction(VaultItemListingsAction.Internal.IconLoadingSettingReceive(it)) }
.launchIn(viewModelScope)
vaultRepository vaultRepository
.vaultDataStateFlow .vaultDataStateFlow
.onEach { sendAction(VaultItemListingsAction.Internal.VaultDataReceive(it)) } .onEach { sendAction(VaultItemListingsAction.Internal.VaultDataReceive(it)) }
@ -54,6 +66,8 @@ class VaultItemListingViewModel @Inject constructor(
is VaultItemListingsAction.AddVaultItemClick -> handleAddVaultItemClick() is VaultItemListingsAction.AddVaultItemClick -> handleAddVaultItemClick()
is VaultItemListingsAction.RefreshClick -> handleRefreshClick() is VaultItemListingsAction.RefreshClick -> handleRefreshClick()
is VaultItemListingsAction.Internal.VaultDataReceive -> handleVaultDataReceive(action) 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) 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 //endregion VaultItemListing Handlers
private fun vaultErrorReceive(vaultData: DataState.Error<VaultData>) { private fun vaultErrorReceive(vaultData: DataState.Error<VaultData>) {
@ -159,7 +185,10 @@ class VaultItemListingViewModel @Inject constructor(
.filter { cipherView -> .filter { cipherView ->
cipherView.determineListingPredicate(currentState.itemListingType) cipherView.determineListingPredicate(currentState.itemListingType)
} }
.toViewState(), .toViewState(
baseIconUrl = state.baseIconUrl,
isIconLoadingDisabled = state.isIconLoadingDisabled,
),
) )
} }
} }
@ -171,6 +200,8 @@ class VaultItemListingViewModel @Inject constructor(
data class VaultItemListingState( data class VaultItemListingState(
val itemListingType: ItemListingType, val itemListingType: ItemListingType,
val viewState: ViewState, val viewState: ViewState,
val baseIconUrl: String,
val isIconLoadingDisabled: Boolean,
) { ) {
/** /**
@ -214,16 +245,13 @@ data class VaultItemListingState(
* @property id the id of the item. * @property id the id of the item.
* @property title title of the item. * @property title title of the item.
* @property subtitle subtitle of the item (nullable). * @property subtitle subtitle of the item (nullable).
* @property uri uri for the icon to be displayed (nullable). * @property iconData data for the icon to be displayed (nullable).
* @property iconRes the icon to be displayed.
*/ */
data class DisplayItem( data class DisplayItem(
val id: String, val id: String,
val title: String, val title: String,
val subtitle: String?, val subtitle: String?,
val uri: String?, val iconData: IconData,
@DrawableRes
val iconRes: Int,
) )
/** /**
@ -399,6 +427,13 @@ sealed class VaultItemListingsAction {
*/ */
sealed class Internal : VaultItemListingsAction() { sealed class Internal : VaultItemListingsAction() {
/**
* Indicates the icon setting was received.
*/
data class IconLoadingSettingReceive(
val isIconLoadingDisabled: Boolean,
) : Internal()
/** /**
* Indicates vault data was received. * 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.CollectionView
import com.bitwarden.core.FolderView import com.bitwarden.core.FolderView
import com.x8bit.bitwarden.R 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.itemlisting.VaultItemListingState
import com.x8bit.bitwarden.ui.vault.feature.vault.util.toLoginIconData
/** /**
* Determines a predicate to filter a list of [CipherView] based on the * 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]. * 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()) { if (isNotEmpty()) {
VaultItemListingState.ViewState.Content(displayItemList = toDisplayItemList()) VaultItemListingState.ViewState.Content(
displayItemList = toDisplayItemList(
baseIconUrl = baseIconUrl,
isIconLoadingDisabled = isIconLoadingDisabled,
),
)
} else { } else {
VaultItemListingState.ViewState.NoItems VaultItemListingState.ViewState.NoItems
} }
@ -82,18 +92,49 @@ fun VaultItemListingState.ItemListingType.updateWithAdditionalDataIfNecessary(
is VaultItemListingState.ItemListingType.Trash -> this is VaultItemListingState.ItemListingType.Trash -> this
} }
private fun List<CipherView>.toDisplayItemList(): List<VaultItemListingState.DisplayItem> = private fun List<CipherView>.toDisplayItemList(
this.map { it.toDisplayItem() } 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( VaultItemListingState.DisplayItem(
id = id.orEmpty(), id = id.orEmpty(),
title = name, title = name,
subtitle = subtitle, subtitle = subtitle,
iconRes = type.iconRes, iconData = this.toIconData(
uri = uri, 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") @Suppress("MagicNumber")
private val CipherView.subtitle: String? private val CipherView.subtitle: String?
get() = when (type) { get() = when (type) {
@ -122,18 +163,3 @@ private val CipherType.iconRes: Int
CipherType.CARD -> R.drawable.ic_card_item CipherType.CARD -> R.drawable.ic_card_item
CipherType.IDENTITY -> R.drawable.ic_identity_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 -> items(state.favoriteItems) { favoriteItem ->
VaultEntryListItem( VaultEntryListItem(
startIcon = painterResource(id = favoriteItem.startIcon), startIcon = favoriteItem.startIcon,
label = favoriteItem.name(), label = favoriteItem.name(),
supportingLabel = favoriteItem.supportingLabel?.invoke(), supportingLabel = favoriteItem.supportingLabel?.invoke(),
onClick = { vaultItemClick(favoriteItem) }, onClick = { vaultItemClick(favoriteItem) },
@ -234,8 +234,9 @@ fun VaultContent(
) )
} }
items(state.noFolderItems) { noFolderItem -> items(state.noFolderItems) { noFolderItem ->
VaultEntryListItem( VaultEntryListItem(
startIcon = painterResource(id = noFolderItem.startIcon), startIcon = noFolderItem.startIcon,
label = noFolderItem.name(), label = noFolderItem.name(),
supportingLabel = noFolderItem.supportingLabel?.invoke(), supportingLabel = noFolderItem.supportingLabel?.invoke(),
onClick = { vaultItemClick(noFolderItem) }, onClick = { vaultItemClick(noFolderItem) },

View file

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

View file

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

View file

@ -1,19 +1,28 @@
package com.x8bit.bitwarden.ui.vault.feature.vault.util package com.x8bit.bitwarden.ui.vault.feature.vault.util
import android.net.Uri
import com.bitwarden.core.CipherType import com.bitwarden.core.CipherType
import com.bitwarden.core.CipherView import com.bitwarden.core.CipherView
import com.bitwarden.core.CollectionView import com.bitwarden.core.CollectionView
import com.bitwarden.core.FolderView 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.data.vault.repository.model.VaultData
import com.x8bit.bitwarden.ui.platform.base.util.asText 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.VaultState
import com.x8bit.bitwarden.ui.vault.feature.vault.model.VaultFilterType 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]. * Transforms [VaultData] into [VaultState.ViewState] using the given [vaultFilterType].
*/ */
fun VaultData.toViewState( fun VaultData.toViewState(
isPremium: Boolean, isPremium: Boolean,
isIconLoadingDisabled: Boolean,
baseIconUrl: String,
vaultFilterType: VaultFilterType, vaultFilterType: VaultFilterType,
): VaultState.ViewState { ): VaultState.ViewState {
val filteredCipherViewList = cipherViewList.toFilteredList(vaultFilterType) val filteredCipherViewList = cipherViewList.toFilteredList(vaultFilterType)
@ -36,7 +45,12 @@ fun VaultData.toViewState(
.count { it.type == CipherType.SECURE_NOTE }, .count { it.type == CipherType.SECURE_NOTE },
favoriteItems = filteredCipherViewList favoriteItems = filteredCipherViewList
.filter { it.favorite } .filter { it.favorite }
.mapNotNull { it.toVaultItemOrNull() }, .mapNotNull {
it.toVaultItemOrNull(
isIconLoadingDisabled = isIconLoadingDisabled,
baseIconUrl = baseIconUrl,
)
},
folderItems = filteredFolderViewList.map { folderView -> folderItems = filteredFolderViewList.map { folderView ->
VaultState.ViewState.FolderItem( VaultState.ViewState.FolderItem(
id = folderView.id, id = folderView.id,
@ -47,7 +61,12 @@ fun VaultData.toViewState(
}, },
noFolderItems = filteredCipherViewList noFolderItems = filteredCipherViewList
.filter { it.folderId.isNullOrBlank() } .filter { it.folderId.isNullOrBlank() }
.mapNotNull { it.toVaultItemOrNull() }, .mapNotNull {
it.toVaultItemOrNull(
isIconLoadingDisabled = isIconLoadingDisabled,
baseIconUrl = baseIconUrl,
)
},
collectionItems = filteredCollectionViewList collectionItems = filteredCollectionViewList
.filter { it.id != null } .filter { it.id != null }
.map { collectionView -> .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]. * Transforms a [CipherView] into a [VaultState.ViewState.VaultItem].
*/ */
@Suppress("MagicNumber") @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 val id = this.id ?: return null
return when (type) { return when (type) {
CipherType.LOGIN -> VaultState.ViewState.VaultItem.Login( CipherType.LOGIN -> VaultState.ViewState.VaultItem.Login(
id = id, id = id,
name = name.asText(), name = name.asText(),
username = login?.username?.asText(), username = login?.username?.asText(),
startIcon = login?.uris.toLoginIconData(
isIconLoadingDisabled = isIconLoadingDisabled,
baseIconUrl = baseIconUrl,
),
) )
CipherType.SECURE_NOTE -> VaultState.ViewState.VaultItem.SecureNote( CipherType.SECURE_NOTE -> VaultState.ViewState.VaultItem.SecureNote(

View file

@ -170,6 +170,39 @@ class EnvironmentUrlsDataJsonExtensionsTest {
(null as EnvironmentUrlDataJson?).toEnvironmentUrlsOrDefault(), (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( private val DEFAULT_CUSTOM_ENVIRONMENT_URL_DATA = EnvironmentUrlDataJson(

View file

@ -85,7 +85,7 @@ fun createMockLoginView(number: Int): LoginView =
*/ */
fun createMockUriView(number: Int): LoginUriView = fun createMockUriView(number: Int): LoginUriView =
LoginUriView( LoginUriView(
uri = "mockUri-$number", uri = "www.mockuri$number.com",
match = UriMatchType.HOST, 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.performClick
import androidx.compose.ui.test.performScrollToNode import androidx.compose.ui.test.performScrollToNode
import com.x8bit.bitwarden.R 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.data.platform.repository.util.bufferedMutableSharedFlow
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
import com.x8bit.bitwarden.ui.platform.base.util.asText 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 com.x8bit.bitwarden.ui.util.isProgressBar
import io.mockk.every import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
@ -393,6 +396,8 @@ class VaultItemListingScreenTest : BaseComposeTest() {
private val DEFAULT_STATE = VaultItemListingState( private val DEFAULT_STATE = VaultItemListingState(
itemListingType = VaultItemListingState.ItemListingType.Login, itemListingType = VaultItemListingState.ItemListingType.Login,
viewState = VaultItemListingState.ViewState.Loading, viewState = VaultItemListingState.ViewState.Loading,
isIconLoadingDisabled = false,
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
) )
private fun createDisplayItem(number: Int): VaultItemListingState.DisplayItem = private fun createDisplayItem(number: Int): VaultItemListingState.DisplayItem =
@ -400,6 +405,5 @@ private fun createDisplayItem(number: Int): VaultItemListingState.DisplayItem =
id = "mockId-$number", id = "mockId-$number",
title = "mockTitle-$number", title = "mockTitle-$number",
subtitle = "mockSubtitle-$number", subtitle = "mockSubtitle-$number",
uri = "mockUri-$number", iconData = IconData.Local(R.drawable.ic_card_item),
iconRes = R.drawable.ic_card_item,
) )

View file

@ -1,9 +1,14 @@
package com.x8bit.bitwarden.ui.vault.feature.itemlisting package com.x8bit.bitwarden.ui.vault.feature.itemlisting
import android.net.Uri
import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.SavedStateHandle
import app.cash.turbine.test import app.cash.turbine.test
import com.x8bit.bitwarden.R 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.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.createMockCipherView
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCollectionView 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.createMockFolderView
@ -17,10 +22,14 @@ import com.x8bit.bitwarden.ui.vault.feature.itemlisting.util.createMockItemListi
import com.x8bit.bitwarden.ui.vault.model.VaultItemListingType import com.x8bit.bitwarden.ui.vault.model.VaultItemListingType
import io.mockk.every import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.unmockkStatic
import io.mockk.verify import io.mockk.verify
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Assertions.assertEquals 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 import org.junit.jupiter.api.Test
class VaultItemListingViewModelTest : BaseViewModelTest() { class VaultItemListingViewModelTest : BaseViewModelTest() {
@ -31,6 +40,16 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
every { vaultDataStateFlow } returns mutableVaultDataStateFlow every { vaultDataStateFlow } returns mutableVaultDataStateFlow
every { sync() } returns Unit 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 initialState = createVaultItemListingState()
private val initialSavedStateHandle = createSavedStateHandleWithVaultItemListingType( private val initialSavedStateHandle = createSavedStateHandleWithVaultItemListingType(
vaultItemListingType = VaultItemListingType.Login, vaultItemListingType = VaultItemListingType.Login,
@ -92,6 +111,8 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
@Test @Test
fun `vaultDataStateFlow Loaded with items should update ViewState to Content`() = fun `vaultDataStateFlow Loaded with items should update ViewState to Content`() =
runTest { runTest {
setupMockUri()
mutableVaultDataStateFlow.tryEmit( mutableVaultDataStateFlow.tryEmit(
value = DataState.Loaded( value = DataState.Loaded(
data = VaultData( data = VaultData(
@ -178,6 +199,8 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
@Test @Test
fun `vaultDataStateFlow Pending with data should update state to Content`() = runTest { fun `vaultDataStateFlow Pending with data should update state to Content`() = runTest {
setupMockUri()
mutableVaultDataStateFlow.tryEmit( mutableVaultDataStateFlow.tryEmit(
value = DataState.Pending( value = DataState.Pending(
data = VaultData( data = VaultData(
@ -267,6 +290,8 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
@Test @Test
fun `vaultDataStateFlow Error with data should update state to Content`() = runTest { fun `vaultDataStateFlow Error with data should update state to Content`() = runTest {
setupMockUri()
mutableVaultDataStateFlow.tryEmit( mutableVaultDataStateFlow.tryEmit(
value = DataState.Error( value = DataState.Error(
data = VaultData( data = VaultData(
@ -291,6 +316,8 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
), ),
viewModel.stateFlow.value, viewModel.stateFlow.value,
) )
unmockkStatic(Uri::class)
} }
@Test @Test
@ -363,6 +390,8 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
@Test @Test
fun `vaultDataStateFlow NoNetwork with data should update state to Content`() = runTest { fun `vaultDataStateFlow NoNetwork with data should update state to Content`() = runTest {
setupMockUri()
mutableVaultDataStateFlow.tryEmit( mutableVaultDataStateFlow.tryEmit(
value = DataState.NoNetwork( value = DataState.NoNetwork(
data = VaultData( data = VaultData(
@ -386,6 +415,8 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
), ),
viewModel.stateFlow.value, viewModel.stateFlow.value,
) )
unmockkStatic(Uri::class)
} }
@Test @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") @Suppress("CyclomaticComplexMethod")
private fun createSavedStateHandleWithVaultItemListingType( private fun createSavedStateHandleWithVaultItemListingType(
vaultItemListingType: VaultItemListingType, 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( private fun createVaultItemListingViewModel(
savedStateHandle: SavedStateHandle = initialSavedStateHandle, savedStateHandle: SavedStateHandle = initialSavedStateHandle,
vaultRepository: VaultRepository = this.vaultRepository, vaultRepository: VaultRepository = this.vaultRepository,
@ -471,6 +519,8 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
VaultItemListingViewModel( VaultItemListingViewModel(
savedStateHandle = savedStateHandle, savedStateHandle = savedStateHandle,
vaultRepository = vaultRepository, vaultRepository = vaultRepository,
environmentRepository = environmentRepository,
settingsRepository = settingsRepository,
) )
@Suppress("MaxLineLength") @Suppress("MaxLineLength")
@ -481,5 +531,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
VaultItemListingState( VaultItemListingState(
itemListingType = itemListingType, itemListingType = itemListingType,
viewState = viewState, 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 package com.x8bit.bitwarden.ui.vault.feature.itemlisting.util
import android.net.Uri
import com.bitwarden.core.CipherType 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.createMockCipherView
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCollectionView 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.createMockFolderView
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.VaultItemListingState 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.Assert.assertEquals
import org.junit.Test import org.junit.Test
@ -244,6 +251,11 @@ class VaultItemListingDataExtensionsTest {
@Test @Test
fun `toViewState should transform a list of CipherViews into a ViewState`() { 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( val cipherViewList = listOf(
createMockCipherView( createMockCipherView(
number = 1, number = 1,
@ -267,7 +279,10 @@ class VaultItemListingDataExtensionsTest {
), ),
) )
val result = cipherViewList.toViewState() val result = cipherViewList.toViewState(
isIconLoadingDisabled = false,
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
)
assertEquals( assertEquals(
VaultItemListingState.ViewState.Content( VaultItemListingState.ViewState.Content(
@ -292,6 +307,8 @@ class VaultItemListingDataExtensionsTest {
), ),
result, result,
) )
unmockkStatic(Uri::class)
} }
@Test @Test

View file

@ -2,6 +2,7 @@ package com.x8bit.bitwarden.ui.vault.feature.itemlisting.util
import com.bitwarden.core.CipherType import com.bitwarden.core.CipherType
import com.x8bit.bitwarden.R 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.itemlisting.VaultItemListingState
/** /**
@ -17,8 +18,10 @@ fun createMockItemListingDisplayItem(
id = "mockId-$number", id = "mockId-$number",
title = "mockName-$number", title = "mockName-$number",
subtitle = "mockUsername-$number", subtitle = "mockUsername-$number",
iconRes = R.drawable.ic_login_item, iconData = IconData.Network(
uri = "mockUri-$number", "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", id = "mockId-$number",
title = "mockName-$number", title = "mockName-$number",
subtitle = null, subtitle = null,
iconRes = R.drawable.ic_secure_note_item, iconData = IconData.Local(R.drawable.ic_secure_note_item),
uri = null,
) )
} }
@ -37,8 +39,7 @@ fun createMockItemListingDisplayItem(
id = "mockId-$number", id = "mockId-$number",
title = "mockName-$number", title = "mockName-$number",
subtitle = "er-$number", subtitle = "er-$number",
iconRes = R.drawable.ic_card_item, iconData = IconData.Local(R.drawable.ic_card_item),
uri = null,
) )
} }
@ -47,8 +48,7 @@ fun createMockItemListingDisplayItem(
id = "mockId-$number", id = "mockId-$number",
title = "mockName-$number", title = "mockName-$number",
subtitle = "mockFirstName-${number}mockLastName-$number", subtitle = "mockFirstName-${number}mockLastName-$number",
iconRes = R.drawable.ic_identity_item, iconData = IconData.Local(R.drawable.ic_identity_item),
uri = null,
) )
} }
} }

View file

@ -16,6 +16,8 @@ import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performScrollToNode import androidx.compose.ui.test.performScrollToNode
import com.x8bit.bitwarden.R 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.data.platform.repository.util.bufferedMutableSharedFlow
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
import com.x8bit.bitwarden.ui.platform.base.util.asText import com.x8bit.bitwarden.ui.platform.base.util.asText
@ -1071,6 +1073,8 @@ private val DEFAULT_STATE: VaultState = VaultState(
viewState = VaultState.ViewState.Loading, viewState = VaultState.ViewState.Loading,
isPremium = false, isPremium = false,
isPullToRefreshSettingEnabled = false, isPullToRefreshSettingEnabled = false,
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
isIconLoadingDisabled = false,
) )
private val DEFAULT_CONTENT_VIEW_STATE: VaultState.ViewState.Content = VaultState.ViewState.Content( 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.SettingsRepository
import com.x8bit.bitwarden.data.platform.repository.model.DataState 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.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.createMockCipherView
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCollectionView 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.createMockFolderView
@ -31,12 +32,15 @@ import io.mockk.verify
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Assertions.assertEquals 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 import org.junit.jupiter.api.Test
@Suppress("LargeClass") @Suppress("LargeClass")
class VaultViewModelTest : BaseViewModelTest() { class VaultViewModelTest : BaseViewModelTest() {
private val mutablePullToRefreshEnabledFlow = MutableStateFlow(false) private val mutablePullToRefreshEnabledFlow = MutableStateFlow(false)
private val mutableIsIconLoadingDisabledFlow = MutableStateFlow(false)
private val mutableUserStateFlow = private val mutableUserStateFlow =
MutableStateFlow<UserState?>(DEFAULT_USER_STATE) MutableStateFlow<UserState?>(DEFAULT_USER_STATE)
@ -57,6 +61,8 @@ class VaultViewModelTest : BaseViewModelTest() {
private val settingsRepository: SettingsRepository = mockk { private val settingsRepository: SettingsRepository = mockk {
every { getPullToRefreshEnabledFlow() } returns mutablePullToRefreshEnabledFlow every { getPullToRefreshEnabledFlow() } returns mutablePullToRefreshEnabledFlow
every { isIconLoadingDisabledFlow } returns mutableIsIconLoadingDisabledFlow
every { isIconLoadingDisabled } returns false
} }
private val vaultRepository: VaultRepository = private val vaultRepository: VaultRepository =
@ -357,6 +363,8 @@ class VaultViewModelTest : BaseViewModelTest() {
viewState = vaultData.toViewState( viewState = vaultData.toViewState(
isPremium = true, isPremium = true,
vaultFilterType = VaultFilterType.AllVaults, vaultFilterType = VaultFilterType.AllVaults,
isIconLoadingDisabled = viewModel.stateFlow.value.isIconLoadingDisabled,
baseIconUrl = viewModel.stateFlow.value.baseIconUrl,
), ),
) )
.copy( .copy(
@ -378,6 +386,8 @@ class VaultViewModelTest : BaseViewModelTest() {
viewState = vaultData.toViewState( viewState = vaultData.toViewState(
isPremium = true, isPremium = true,
vaultFilterType = VaultFilterType.MyVault, vaultFilterType = VaultFilterType.MyVault,
isIconLoadingDisabled = viewModel.stateFlow.value.isIconLoadingDisabled,
baseIconUrl = viewModel.stateFlow.value.baseIconUrl,
), ),
), ),
viewModel.stateFlow.value, 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 = private fun createViewModel(): VaultViewModel =
VaultViewModel( VaultViewModel(
authRepository = authRepository, authRepository = authRepository,
@ -1120,4 +1140,6 @@ private fun createMockVaultState(
isSwitchingAccounts = false, isSwitchingAccounts = false,
isPremium = true, isPremium = true,
isPullToRefreshSettingEnabled = false, isPullToRefreshSettingEnabled = false,
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
isIconLoadingDisabled = false,
) )

View file

@ -1,13 +1,25 @@
package com.x8bit.bitwarden.ui.vault.feature.vault.util 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.createMockCipherView
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCollectionView 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.createMockFolderView
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSendView import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSendView
import com.x8bit.bitwarden.data.vault.repository.model.VaultData import com.x8bit.bitwarden.data.vault.repository.model.VaultData
import com.x8bit.bitwarden.ui.platform.base.util.asText 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.VaultState
import com.x8bit.bitwarden.ui.vault.feature.vault.model.VaultFilterType 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.Assertions.assertEquals
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
@ -25,6 +37,8 @@ class VaultDataExtensionsTest {
val actual = vaultData.toViewState( val actual = vaultData.toViewState(
isPremium = true, isPremium = true,
isIconLoadingDisabled = false,
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
vaultFilterType = VaultFilterType.AllVaults, vaultFilterType = VaultFilterType.AllVaults,
) )
@ -72,6 +86,8 @@ class VaultDataExtensionsTest {
val actual = vaultData.toViewState( val actual = vaultData.toViewState(
isPremium = true, isPremium = true,
isIconLoadingDisabled = false,
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
vaultFilterType = VaultFilterType.MyVault, vaultFilterType = VaultFilterType.MyVault,
) )
@ -116,6 +132,8 @@ class VaultDataExtensionsTest {
val actual = vaultData.toViewState( val actual = vaultData.toViewState(
isPremium = true, isPremium = true,
isIconLoadingDisabled = false,
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
vaultFilterType = VaultFilterType.OrganizationVault( vaultFilterType = VaultFilterType.OrganizationVault(
organizationId = "mockOrganizationId-1", organizationId = "mockOrganizationId-1",
organizationName = "Mock Organization 1", organizationName = "Mock Organization 1",
@ -156,6 +174,8 @@ class VaultDataExtensionsTest {
val actual = vaultData.toViewState( val actual = vaultData.toViewState(
isPremium = true, isPremium = true,
isIconLoadingDisabled = false,
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
vaultFilterType = VaultFilterType.AllVaults, vaultFilterType = VaultFilterType.AllVaults,
) )
@ -176,6 +196,8 @@ class VaultDataExtensionsTest {
val actual = vaultData.toViewState( val actual = vaultData.toViewState(
isPremium = true, isPremium = true,
isIconLoadingDisabled = false,
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
vaultFilterType = VaultFilterType.AllVaults, vaultFilterType = VaultFilterType.AllVaults,
) )
@ -197,6 +219,8 @@ class VaultDataExtensionsTest {
val actual = vaultData.toViewState( val actual = vaultData.toViewState(
isPremium = true, isPremium = true,
isIconLoadingDisabled = false,
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
vaultFilterType = VaultFilterType.AllVaults, vaultFilterType = VaultFilterType.AllVaults,
) )
@ -230,6 +254,8 @@ class VaultDataExtensionsTest {
val actual = vaultData.toViewState( val actual = vaultData.toViewState(
isPremium = false, isPremium = false,
vaultFilterType = VaultFilterType.AllVaults, vaultFilterType = VaultFilterType.AllVaults,
isIconLoadingDisabled = false,
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
) )
assertEquals( assertEquals(
@ -248,4 +274,111 @@ class VaultDataExtensionsTest {
actual, 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)
}
} }