1
0
Fork 0
mirror of https://github.com/bitwarden/android.git synced 2025-02-23 00:59:16 +03:00

Merge branch 'main' into renovate/gradle-minor

This commit is contained in:
David Perez 2024-12-02 15:42:33 -06:00 committed by GitHub
commit fc08907b8a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
51 changed files with 632 additions and 215 deletions
.github/workflows
Gemfile.lock
app/src
main
test/java/com/x8bit/bitwarden

View file

@ -88,7 +88,7 @@ jobs:
path: app/build/reports/tests/
- name: Upload to codecov.io
uses: codecov/codecov-action@b9fd7d16f6d7d1b5d2bec1a2887e65ceed900238 # v4.6.0
uses: codecov/codecov-action@015f24e6818733317a2da2edd6290ab26238649a # v5.0.7
with:
file: app/build/reports/kover/reportStandardDebug.xml
env:

View file

@ -10,16 +10,16 @@ GEM
artifactory (3.0.17)
atomos (0.1.3)
aws-eventstream (1.3.0)
aws-partitions (1.1003.0)
aws-sdk-core (3.212.0)
aws-partitions (1.1013.0)
aws-sdk-core (3.213.0)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.992.0)
aws-sigv4 (~> 1.9)
jmespath (~> 1, >= 1.6.1)
aws-sdk-kms (1.95.0)
aws-sdk-kms (1.96.0)
aws-sdk-core (~> 3, >= 3.210.0)
aws-sigv4 (~> 1.5)
aws-sdk-s3 (1.170.0)
aws-sdk-s3 (1.173.0)
aws-sdk-core (~> 3, >= 3.210.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.5)
@ -162,7 +162,7 @@ GEM
domain_name (~> 0.5)
httpclient (2.8.3)
jmespath (1.6.2)
json (2.8.1)
json (2.8.2)
jwt (2.9.3)
base64
mini_magick (4.13.2)

View file

@ -18,4 +18,5 @@ data class Organization(
val shouldManageResetPassword: Boolean,
val shouldUseKeyConnector: Boolean,
val role: OrganizationType,
val shouldUsersGetPremium: Boolean,
)

View file

@ -22,6 +22,7 @@ fun SyncResponseJson.Profile.Organization.toOrganization(): Organization =
shouldUseKeyConnector = this.shouldUseKeyConnector,
role = this.type,
shouldManageResetPassword = this.permissions.shouldManageResetPassword,
shouldUsersGetPremium = this.shouldUsersGetPremium,
)
/**

View file

@ -9,6 +9,7 @@ import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardMan
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
import com.x8bit.bitwarden.data.vault.repository.model.GenerateTotpResult
import com.x8bit.bitwarden.ui.vault.feature.vault.util.getOrganizationPremiumStatusMap
import java.time.Clock
/**
@ -24,8 +25,15 @@ class AutofillTotpManagerImpl(
) : AutofillTotpManager {
override suspend fun tryCopyTotpToClipboard(cipherView: CipherView) {
if (settingsRepository.isAutoCopyTotpDisabled) return
val organizationPremiumStatusMap = authRepository
.userStateFlow
.value
?.activeAccount
?.getOrganizationPremiumStatusMap()
.orEmpty()
val isPremium = authRepository.userStateFlow.value?.activeAccount?.isPremium == true
if (!isPremium && !cipherView.organizationUseTotp) return
val premiumStatus = organizationPremiumStatusMap[cipherView.organizationId] ?: isPremium
if (!premiumStatus && !cipherView.organizationUseTotp) return
val totpCode = cipherView.login?.totp ?: return
val totpResult = vaultRepository.generateTotp(

View file

@ -122,6 +122,7 @@ class TotpCodeManagerImpl(
CipherRepromptType.NONE -> false
},
orgUsesTotp = cipher.organizationUseTotp,
orgId = cipher.organizationId,
)
}
.onFailure {

View file

@ -29,4 +29,5 @@ data class VerificationCodeItem(
val username: String?,
val hasPasswordReprompt: Boolean,
val orgUsesTotp: Boolean,
val orgId: String?,
)

View file

@ -46,6 +46,7 @@ import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenFilledButton
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenTextButton
import com.x8bit.bitwarden.ui.platform.components.card.BitwardenActionCardSmall
import com.x8bit.bitwarden.ui.platform.components.card.color.bitwardenCardColors
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog
@ -211,6 +212,10 @@ private fun CompleteRegistrationContent(
actionIcon = rememberVectorPainter(id = R.drawable.ic_question_circle),
actionText = stringResource(id = R.string.what_makes_a_password_strong),
callToActionText = stringResource(id = R.string.learn_more),
callToActionTextColor = BitwardenTheme.colorScheme.text.interaction,
colors = bitwardenCardColors(
containerColor = BitwardenTheme.colorScheme.background.primary,
),
onCardClicked = handler.onMakeStrongPassword,
modifier = Modifier
.fillMaxWidth()

View file

@ -1,52 +1,46 @@
package com.x8bit.bitwarden.ui.auth.feature.masterpasswordguidance
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.clearAndSetSemantics
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
import com.x8bit.bitwarden.ui.platform.base.util.bitwardenBoldSpanStyle
import com.x8bit.bitwarden.ui.platform.base.util.createAnnotatedString
import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin
import com.x8bit.bitwarden.ui.platform.base.util.toAnnotatedString
import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
import com.x8bit.bitwarden.ui.platform.components.card.BitwardenActionCardSmall
import com.x8bit.bitwarden.ui.platform.components.divider.BitwardenHorizontalDivider
import com.x8bit.bitwarden.ui.platform.components.card.BitwardenActionCard
import com.x8bit.bitwarden.ui.platform.components.card.BitwardenContentCard
import com.x8bit.bitwarden.ui.platform.components.model.ContentBlockData
import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
private const val BULLET_TWO_TAB = "\u2022\t\t"
import kotlinx.collections.immutable.persistentListOf
/**
* The top level composable for the Master Password Guidance screen.
*/
@OptIn(ExperimentalMaterial3Api::class)
@Suppress("LongMethod")
@Composable
fun MasterPasswordGuidanceScreen(
onNavigateBack: () -> Unit,
@ -81,126 +75,126 @@ fun MasterPasswordGuidanceScreen(
)
},
) {
Column(
MasterPasswordGuidanceContent(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.standardHorizontalMargin(),
) {
Column(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(size = 4.dp))
.background(BitwardenTheme.colorScheme.background.tertiary),
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(all = 24.dp),
) {
Text(
text = stringResource(R.string.what_makes_a_password_strong),
style = BitwardenTheme.typography.titleMedium,
color = BitwardenTheme.colorScheme.text.primary,
)
Spacer(modifier = Modifier.height(8.dp))
Text(
style = BitwardenTheme.typography.bodyMedium,
color = BitwardenTheme.colorScheme.text.primary,
text = stringResource(
R.string.the_longer_your_password_the_more_difficult_to_hack,
),
)
}
BitwardenHorizontalDivider()
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 24.dp, vertical = 16.dp),
) {
Text(
text = stringResource(R.string.the_strongest_passwords_are_usually),
style = BitwardenTheme.typography.titleSmall,
color = BitwardenTheme.colorScheme.text.primary,
)
Spacer(modifier = Modifier.height(8.dp))
BulletTextRow(text = stringResource(R.string.twelve_or_more_characters))
BulletTextRow(
text = stringResource(
R.string.random_and_complex_using_numbers_and_special_characters,
),
)
BulletTextRow(
text = stringResource(R.string.totally_different_from_your_other_passwords),
)
}
}
Spacer(modifier = Modifier.height(16.dp))
TryGeneratorCard(
onCardClicked = remember(viewModel) {
{
viewModel.trySendAction(
MasterPasswordGuidanceAction.TryPasswordGeneratorAction,
)
}
},
)
Spacer(modifier = Modifier.navigationBarsPadding())
}
onTryPasswordGeneratorAction = {
viewModel.trySendAction(
MasterPasswordGuidanceAction.TryPasswordGeneratorAction,
)
},
)
}
}
@Composable
private fun TryGeneratorCard(
onCardClicked: () -> Unit,
private fun MasterPasswordGuidanceContent(
modifier: Modifier = Modifier,
onTryPasswordGeneratorAction: () -> Unit,
) {
BitwardenActionCardSmall(
actionIcon = rememberVectorPainter(id = R.drawable.ic_generate),
actionText = stringResource(
R.string.use_the_generator_to_create_a_strong_unique_password,
),
callToActionText = stringResource(R.string.try_it_out),
onCardClicked = onCardClicked,
modifier = modifier
.fillMaxWidth(),
trailingContent = {
Icon(
painter = rememberVectorPainter(id = R.drawable.ic_chevron_right),
contentDescription = null,
tint = BitwardenTheme.colorScheme.icon.primary,
modifier = Modifier
.align(Alignment.Center)
.size(16.dp),
)
},
)
}
@Composable
private fun BulletTextRow(
text: String,
modifier: Modifier = Modifier,
) {
Row(
modifier = modifier
.fillMaxWidth()
.padding(horizontal = 8.dp),
) {
Column(modifier = modifier) {
Spacer(modifier = Modifier.height(24.dp))
Text(
text = BULLET_TWO_TAB,
text = stringResource(R.string.a_secure_memorable_password),
textAlign = TextAlign.Center,
style = BitwardenTheme.typography.titleMedium,
color = BitwardenTheme.colorScheme.text.primary,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(
R.string.one_of_the_best_ways_to_create_a_secure_and_memorable_password,
),
textAlign = TextAlign.Center,
style = BitwardenTheme.typography.bodyMedium,
color = BitwardenTheme.colorScheme.text.primary,
modifier = Modifier.clearAndSetSemantics { },
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
)
Text(
text = text,
style = BitwardenTheme.typography.bodyMedium,
color = BitwardenTheme.colorScheme.text.primary,
Spacer(modifier = Modifier.height(24.dp))
MasterPasswordGuidanceContentBlocks()
NeedSomeInspirationCard(
onActionClicked = {
onTryPasswordGeneratorAction()
},
)
Spacer(modifier = Modifier.navigationBarsPadding())
}
}
@Composable
private fun MasterPasswordGuidanceContentBlocks(modifier: Modifier = Modifier) {
Column(modifier = modifier) {
BitwardenContentCard(
contentItems = persistentListOf(
ContentBlockData(
headerText = stringResource(R.string.choose_three_or_four_random_words)
.toAnnotatedString(),
subtitleText = createAnnotatedString(
mainString = stringResource(
R.string.pick_three_or_four_random_unrelated_words,
),
highlights = listOf(
stringResource(
R.string.pick_three_or_four_random_unrelated_words_highlight,
),
),
highlightStyle = bitwardenBoldSpanStyle,
),
iconVectorResource = R.drawable.ic_number1,
),
ContentBlockData(
headerText = stringResource(R.string.combine_those_words_together)
.toAnnotatedString(),
subtitleText = createAnnotatedString(
mainString = stringResource(
R.string.put_the_words_together_in_any_order_to_form_your_passphrase,
),
highlights = listOf(
stringResource(
R.string.use_hyphens_spaces_or_leave_them_as_long_word_highlight,
),
),
highlightStyle = bitwardenBoldSpanStyle,
),
iconVectorResource = R.drawable.ic_number2,
),
ContentBlockData(
headerText = stringResource(R.string.make_it_yours).toAnnotatedString(),
subtitleText = createAnnotatedString(
mainString = stringResource(
R.string.add_a_number_or_symbol_to_make_it_even_stronger,
),
highlights = listOf(
stringResource(R.string.add_a_number_or_symbol_highlight),
),
highlightStyle = bitwardenBoldSpanStyle,
),
iconVectorResource = R.drawable.ic_number3,
),
),
)
}
Spacer(modifier = Modifier.height(24.dp))
}
@Composable
private fun NeedSomeInspirationCard(
onActionClicked: () -> Unit,
modifier: Modifier = Modifier,
) {
BitwardenActionCard(
cardTitle = stringResource(R.string.need_some_inspiration),
actionText = stringResource(R.string.check_out_the_passphrase_generator),
onActionClick = onActionClicked,
modifier = modifier.fillMaxWidth(),
)
}
@Preview

View file

@ -35,7 +35,7 @@ import kotlin.let
* @param cardTitle The title of the card.
* @param actionText The text content on the CTA button.
* @param onActionClick The action to perform when the CTA button is clicked.
* @param onDismissClick The action to perform when the dismiss button is clicked.
* @param onDismissClick Optional action to perform when the dismiss button is clicked.
* @param leadingContent Optional content to display on the leading side of the
* [cardTitle] [Text].
*/
@ -44,8 +44,8 @@ fun BitwardenActionCard(
cardTitle: String,
actionText: String,
onActionClick: () -> Unit,
onDismissClick: () -> Unit,
modifier: Modifier = Modifier,
onDismissClick: (() -> Unit)? = null,
cardSubtitle: String? = null,
leadingContent: @Composable (() -> Unit)? = null,
) {
@ -69,11 +69,13 @@ fun BitwardenActionCard(
)
}
Spacer(Modifier.weight(1f))
BitwardenStandardIconButton(
painter = rememberVectorPainter(id = R.drawable.ic_close),
contentDescription = stringResource(id = R.string.close),
onClick = onDismissClick,
)
onDismissClick?.let {
BitwardenStandardIconButton(
painter = rememberVectorPainter(id = R.drawable.ic_close),
contentDescription = stringResource(id = R.string.close),
onClick = it,
)
}
}
cardSubtitle?.let {
Spacer(Modifier.height(4.dp))
@ -103,6 +105,19 @@ fun BitwardenActionCard(
*/
fun actionCardExitAnimation() = fadeOut() + shrinkVertically(shrinkTowards = Alignment.Top)
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun BitwardenActionCardWithSubtitleNoDismiss_preview() {
BitwardenTheme {
BitwardenActionCard(
cardTitle = "Title",
actionText = "Action",
onActionClick = {},
)
}
}
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable

View file

@ -12,12 +12,14 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material3.Card
import androidx.compose.material3.CardColors
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.VectorPainter
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
@ -34,6 +36,8 @@ fun BitwardenActionCardSmall(
actionIcon: VectorPainter,
actionText: String,
callToActionText: String,
callToActionTextColor: Color = BitwardenTheme.colorScheme.text.primary,
colors: CardColors = bitwardenCardColors(),
onCardClicked: () -> Unit,
modifier: Modifier = Modifier,
trailingContent: (@Composable BoxScope.() -> Unit)? = null,
@ -42,7 +46,7 @@ fun BitwardenActionCardSmall(
onClick = onCardClicked,
shape = BitwardenTheme.shapes.actionCard,
modifier = modifier,
colors = bitwardenCardColors(),
colors = colors,
elevation = CardDefaults.elevatedCardElevation(),
border = BorderStroke(width = 1.dp, color = BitwardenTheme.colorScheme.stroke.border),
) {
@ -70,7 +74,7 @@ fun BitwardenActionCardSmall(
Text(
text = callToActionText,
style = BitwardenTheme.typography.labelLarge,
color = BitwardenTheme.colorScheme.text.primary,
color = callToActionTextColor,
)
}
Spacer(modifier = Modifier.width(16.dp))

View file

@ -2,15 +2,23 @@ package com.x8bit.bitwarden.ui.platform.components.card.color
import androidx.compose.material3.CardColors
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
/**
* Provides a default set of Bitwarden-styled colors for a card.
*/
@Composable
fun bitwardenCardColors(): CardColors = CardColors(
containerColor = BitwardenTheme.colorScheme.background.tertiary,
contentColor = BitwardenTheme.colorScheme.text.primary,
disabledContainerColor = BitwardenTheme.colorScheme.filledButton.backgroundDisabled,
disabledContentColor = BitwardenTheme.colorScheme.filledButton.foregroundDisabled,
)
fun bitwardenCardColors(
containerColor: Color = BitwardenTheme.colorScheme.background.tertiary,
contentColor: Color = BitwardenTheme.colorScheme.text.primary,
disabledContainerColor: Color = BitwardenTheme.colorScheme.filledButton.backgroundDisabled,
disabledContentColor: Color = BitwardenTheme.colorScheme.filledButton.foregroundDisabled,
): CardColors {
return CardColors(
containerColor = containerColor,
contentColor = contentColor,
disabledContainerColor = disabledContainerColor,
disabledContentColor = disabledContentColor,
)
}

View file

@ -46,6 +46,7 @@ import com.x8bit.bitwarden.ui.platform.feature.search.util.updateWithAdditionalD
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.model.ListingItemOverflowAction
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.getOrganizationPremiumStatusMap
import com.x8bit.bitwarden.ui.vault.feature.vault.util.toFilteredList
import com.x8bit.bitwarden.ui.vault.feature.vault.util.toVaultFilterData
import com.x8bit.bitwarden.ui.vault.model.TotpData
@ -107,6 +108,9 @@ class SearchViewModel @Inject constructor(
totpData = specialCircumstance?.toTotpDataOrNull(),
hasMasterPassword = userState.activeAccount.hasMasterPassword,
isPremium = userState.activeAccount.isPremium,
organizationPremiumStatusMap = userState
.activeAccount
.getOrganizationPremiumStatusMap(),
)
},
) {
@ -687,6 +691,7 @@ class SearchViewModel @Inject constructor(
isAutofill = state.isAutofill,
isTotp = state.isTotp,
isPremiumUser = state.isPremium,
organizationPremiumStatusMap = state.organizationPremiumStatusMap,
)
}
@ -733,6 +738,7 @@ data class SearchState(
val totpData: TotpData?,
val hasMasterPassword: Boolean,
val isPremium: Boolean,
val organizationPremiumStatusMap: Map<String, Boolean>,
) : Parcelable {
/**

View file

@ -152,6 +152,7 @@ fun List<CipherView>.toViewState(
isAutofill: Boolean,
isTotp: Boolean,
isPremiumUser: Boolean,
organizationPremiumStatusMap: Map<String, Boolean>,
): SearchState.ViewState =
when {
searchTerm.isEmpty() -> SearchState.ViewState.Empty(message = null)
@ -164,6 +165,7 @@ fun List<CipherView>.toViewState(
isAutofill = isAutofill,
isTotp = isTotp,
isPremiumUser = isPremiumUser,
organizationPremiumStatusMap = organizationPremiumStatusMap,
)
.sortAlphabetically(),
)
@ -184,15 +186,17 @@ private fun List<CipherView>.toDisplayItemList(
isAutofill: Boolean,
isTotp: Boolean,
isPremiumUser: Boolean,
organizationPremiumStatusMap: Map<String, Boolean>,
): List<SearchState.DisplayItem> =
this.map {
val premiumStatus = organizationPremiumStatusMap[it.organizationId] ?: isPremiumUser
it.toDisplayItem(
baseIconUrl = baseIconUrl,
hasMasterPassword = hasMasterPassword,
isIconLoadingDisabled = isIconLoadingDisabled,
isAutofill = isAutofill,
isTotp = isTotp,
isPremiumUser = isPremiumUser,
isPremiumUser = premiumStatus,
)
}

View file

@ -404,6 +404,7 @@ private fun VaultItemContent(
VaultItemSshKeyContent(
commonState = viewState.common,
sshKeyItemState = viewState.type,
vaultCommonItemTypeHandlers = vaultCommonItemTypeHandlers,
vaultSshKeyItemTypeHandlers = vaultSshKeyItemTypeHandlers,
modifier = modifier,
)

View file

@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
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.platform.testTag
@ -18,6 +19,7 @@ import com.x8bit.bitwarden.ui.platform.components.field.BitwardenPasswordField
import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextField
import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextFieldWithActions
import com.x8bit.bitwarden.ui.platform.components.header.BitwardenListHeaderText
import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultCommonItemTypeHandlers
import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultSshKeyItemTypeHandlers
/**
@ -29,6 +31,7 @@ fun VaultItemSshKeyContent(
commonState: VaultItemState.ViewState.Content.Common,
sshKeyItemState: VaultItemState.ViewState.Content.ItemType.SshKey,
vaultSshKeyItemTypeHandlers: VaultSshKeyItemTypeHandlers,
vaultCommonItemTypeHandlers: VaultCommonItemTypeHandlers,
modifier: Modifier = Modifier,
) {
LazyColumn(modifier = modifier) {
@ -120,6 +123,85 @@ fun VaultItemSshKeyContent(
)
}
commonState.notes?.let { notes ->
item {
Spacer(modifier = Modifier.height(4.dp))
BitwardenListHeaderText(
label = stringResource(id = R.string.notes),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
Spacer(modifier = Modifier.height(8.dp))
BitwardenTextFieldWithActions(
label = stringResource(id = R.string.notes),
value = notes,
onValueChange = { },
readOnly = true,
singleLine = false,
actions = {
BitwardenTonalIconButton(
vectorIconRes = R.drawable.ic_copy,
contentDescription = stringResource(id = R.string.copy_notes),
onClick = vaultCommonItemTypeHandlers.onCopyNotesClick,
modifier = Modifier.testTag(tag = "CipherNotesCopyButton"),
)
},
textFieldTestTag = "CipherNotesLabel",
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
)
}
}
commonState.customFields.takeUnless { it.isEmpty() }?.let { customFields ->
item {
Spacer(modifier = Modifier.height(4.dp))
BitwardenListHeaderText(
label = stringResource(id = R.string.custom_fields),
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
)
}
items(customFields) { customField ->
Spacer(modifier = Modifier.height(8.dp))
CustomField(
customField = customField,
onCopyCustomHiddenField = vaultCommonItemTypeHandlers.onCopyCustomHiddenField,
onCopyCustomTextField = vaultCommonItemTypeHandlers.onCopyCustomTextField,
onShowHiddenFieldClick = vaultCommonItemTypeHandlers.onShowHiddenFieldClick,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
)
}
}
commonState.attachments.takeUnless { it?.isEmpty() == true }?.let { attachments ->
item {
Spacer(modifier = Modifier.height(4.dp))
BitwardenListHeaderText(
label = stringResource(id = R.string.attachments),
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
)
}
items(attachments) { attachmentItem ->
AttachmentItemContent(
modifier = Modifier
.fillMaxWidth()
.padding(start = 16.dp),
attachmentItem = attachmentItem,
onAttachmentDownloadClick =
vaultCommonItemTypeHandlers.onAttachmentDownloadClick,
)
}
item { Spacer(modifier = Modifier.height(8.dp)) }
}
item {
Spacer(modifier = Modifier.height(24.dp))
VaultItemUpdateText(

View file

@ -8,6 +8,7 @@ import com.bitwarden.vault.CipherView
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.model.BreachCountResult
import com.x8bit.bitwarden.data.auth.repository.model.Organization
import com.x8bit.bitwarden.data.auth.repository.model.UserState
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePasswordResult
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager
@ -1028,14 +1029,19 @@ class VaultItemViewModel @Inject constructor(
): VaultItemState.ViewState = this
.data
?.cipher
?.toViewState(
previousState = state.viewState.asContentOrNull(),
isPremiumUser = account.isPremium,
hasMasterPassword = account.hasMasterPassword,
totpCodeItemData = this.data?.totpCodeItemData,
canDelete = this.data?.canDelete == true,
canAssignToCollections = this.data?.canAssociateToCollections == true,
)
?.let { cipher ->
val ownerOrg: Organization? = account.organizations.find {
cipher.organizationId == it.id
}
cipher.toViewState(
previousState = state.viewState.asContentOrNull(),
isPremiumUser = ownerOrg?.shouldUsersGetPremium ?: account.isPremium,
hasMasterPassword = account.hasMasterPassword,
totpCodeItemData = this.data?.totpCodeItemData,
canDelete = this.data?.canDelete == true,
canAssignToCollections = this.data?.canAssociateToCollections == true,
)
}
?: VaultItemState.ViewState.Error(message = errorText)
private fun handleValidatePasswordReceive(

View file

@ -13,9 +13,9 @@ import com.x8bit.bitwarden.data.auth.repository.model.ValidatePasswordResult
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePinResult
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilitySelectionManager
import com.x8bit.bitwarden.data.autofill.fido2.manager.Fido2CredentialManager
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CreateCredentialRequest
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialAssertionRequest
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialAssertionResult
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CreateCredentialRequest
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2GetCredentialsRequest
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2GetCredentialsResult
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2RegisterCredentialResult
@ -70,6 +70,7 @@ import com.x8bit.bitwarden.ui.vault.feature.itemlisting.util.toVaultItemCipherTy
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.util.toViewState
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.util.updateWithAdditionalDataIfNecessary
import com.x8bit.bitwarden.ui.vault.feature.vault.model.VaultFilterType
import com.x8bit.bitwarden.ui.vault.feature.vault.util.getOrganizationPremiumStatusMap
import com.x8bit.bitwarden.ui.vault.feature.vault.util.toAccountSummaries
import com.x8bit.bitwarden.ui.vault.feature.vault.util.toActiveAccountSummary
import com.x8bit.bitwarden.ui.vault.feature.vault.util.toFilteredList
@ -138,6 +139,9 @@ class VaultItemListingViewModel @Inject constructor(
fido2GetCredentialsRequest = specialCircumstance?.toFido2GetCredentialsRequestOrNull(),
isPremium = userState.activeAccount.isPremium,
isRefreshing = false,
organizationPremiumStatusMap = userState
.activeAccount
.getOrganizationPremiumStatusMap(),
)
},
) {
@ -1580,6 +1584,7 @@ class VaultItemListingViewModel @Inject constructor(
.fido2CredentialAutofillViewList,
totpData = state.totpData,
isPremiumUser = state.isPremium,
organizationPremiumStatusMap = state.organizationPremiumStatusMap,
)
}
@ -1745,6 +1750,7 @@ data class VaultItemListingState(
val hasMasterPassword: Boolean,
val isPremium: Boolean,
val isRefreshing: Boolean,
val organizationPremiumStatusMap: Map<String, Boolean>,
) {
/**
* Whether or not the add FAB should be shown.

View file

@ -111,6 +111,7 @@ fun VaultData.toViewState(
fido2CredentialAutofillViews: List<Fido2CredentialAutofillView>?,
totpData: TotpData?,
isPremiumUser: Boolean,
organizationPremiumStatusMap: Map<String, Boolean>,
): VaultItemListingState.ViewState {
val filteredCipherViewList = cipherViewList
.filter { cipherView ->
@ -142,6 +143,7 @@ fun VaultData.toViewState(
fido2CredentialAutofillViews = fido2CredentialAutofillViews,
isPremiumUser = isPremiumUser,
isTotp = totpData != null,
organizationPremiumStatusMap = organizationPremiumStatusMap,
),
displayFolderList = folderList.map { folderView ->
VaultItemListingState.FolderDisplayItem(
@ -290,8 +292,10 @@ private fun List<CipherView>.toDisplayItemList(
fido2CredentialAutofillViews: List<Fido2CredentialAutofillView>?,
isPremiumUser: Boolean,
isTotp: Boolean,
organizationPremiumStatusMap: Map<String, Boolean>,
): List<VaultItemListingState.DisplayItem> =
this.map {
val premiumStatus = organizationPremiumStatusMap[it.organizationId] ?: isPremiumUser
it.toDisplayItem(
baseIconUrl = baseIconUrl,
hasMasterPassword = hasMasterPassword,
@ -302,7 +306,7 @@ private fun List<CipherView>.toDisplayItemList(
?.firstOrNull { fido2CredentialAutofillView ->
fido2CredentialAutofillView.cipherId == it.id
},
isPremiumUser = isPremiumUser,
isPremiumUser = premiumStatus,
isTotp = isTotp,
)
}

View file

@ -37,6 +37,7 @@ import com.x8bit.bitwarden.ui.platform.manager.snackbar.SnackbarRelayManager
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.model.ListingItemOverflowAction
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.getOrganizationPremiumStatusMap
import com.x8bit.bitwarden.ui.vault.feature.vault.util.initials
import com.x8bit.bitwarden.ui.vault.feature.vault.util.toAccountSummaries
import com.x8bit.bitwarden.ui.vault.feature.vault.util.toActiveAccountSummary
@ -102,6 +103,9 @@ class VaultViewModel @Inject constructor(
isRefreshing = false,
showImportActionCard = false,
showSshKeys = showSshKeys,
organizationPremiumStatusMap = userState
.activeAccount
.getOrganizationPremiumStatusMap(),
)
},
) {
@ -608,6 +612,7 @@ class VaultViewModel @Inject constructor(
hasMasterPassword = state.hasMasterPassword,
vaultFilterType = vaultFilterTypeOrDefault,
showSshKeys = showSshKeys,
organizationPremiumStatusMap = state.organizationPremiumStatusMap,
),
dialog = null,
isRefreshing = false,
@ -644,6 +649,7 @@ class VaultViewModel @Inject constructor(
hasMasterPassword = state.hasMasterPassword,
vaultFilterType = vaultFilterTypeOrDefault,
showSshKeys = state.showSshKeys,
organizationPremiumStatusMap = state.organizationPremiumStatusMap,
),
)
}
@ -715,6 +721,7 @@ data class VaultState(
val isRefreshing: Boolean,
val showImportActionCard: Boolean,
val showSshKeys: Boolean,
val organizationPremiumStatusMap: Map<String, Boolean>,
) : Parcelable {
/**
@ -1334,6 +1341,7 @@ private fun MutableStateFlow<VaultState>.updateToErrorStateOrDialog(
vaultFilterType = vaultFilterType,
isIconLoadingDisabled = isIconLoadingDisabled,
showSshKeys = it.showSshKeys,
organizationPremiumStatusMap = it.organizationPremiumStatusMap,
),
dialog = VaultState.DialogState.Error(
title = errorTitle,

View file

@ -71,3 +71,11 @@ fun UserState.Account.toVaultFilterData(
),
)
}
/**
* Returns a map of organization IDs and if they provide a premium status to the user for
* items owned by that organization.
*/
fun UserState.Account.getOrganizationPremiumStatusMap(): Map<String, Boolean> {
return organizations.associate { it.id to it.shouldUsersGetPremium }
}

View file

@ -40,6 +40,7 @@ fun VaultData.toViewState(
baseIconUrl: String,
vaultFilterType: VaultFilterType,
showSshKeys: Boolean,
organizationPremiumStatusMap: Map<String, Boolean>,
): VaultState.ViewState {
val filteredCipherViewListWithDeletedItems =
@ -73,14 +74,22 @@ fun VaultData.toViewState(
return if (filteredCipherViewListWithDeletedItems.isEmpty()) {
VaultState.ViewState.NoItems
} else {
val totpItems = filteredCipherViewList.filter { it.login?.totp != null }
val totpItemsGroupedByOwnership = filteredCipherViewList.groupBy {
!it.organizationId.isNullOrBlank()
}
val userOwnedTotpItems = totpItemsGroupedByOwnership[false]
?.filter {
it.login?.totp != null && isPremium
}.orEmpty()
val organizationOwnedTotpItems = totpItemsGroupedByOwnership[true]
?.filter {
it.login?.totp != null &&
(organizationPremiumStatusMap[it.id] == true || it.organizationUseTotp)
}.orEmpty()
VaultState.ViewState.Content(
itemTypesCount = itemTypesCount,
totpItemsCount = if (isPremium) {
totpItems.count()
} else {
totpItems.count { it.organizationUseTotp }
},
totpItemsCount = userOwnedTotpItems.count() +
organizationOwnedTotpItems.count(),
loginItemsCount = filteredCipherViewList.count { it.type == CipherType.LOGIN },
cardItemsCount = filteredCipherViewList.count { it.type == CipherType.CARD },
identityItemsCount = filteredCipherViewList.count { it.type == CipherType.IDENTITY },
@ -94,7 +103,8 @@ fun VaultData.toViewState(
hasMasterPassword = hasMasterPassword,
isIconLoadingDisabled = isIconLoadingDisabled,
baseIconUrl = baseIconUrl,
isPremiumUser = isPremium,
isPremiumUser = organizationPremiumStatusMap[it.organizationId]
?: isPremium,
)
},
folderItems = filteredFolderViewList
@ -128,7 +138,8 @@ fun VaultData.toViewState(
hasMasterPassword = hasMasterPassword,
isIconLoadingDisabled = isIconLoadingDisabled,
baseIconUrl = baseIconUrl,
isPremiumUser = isPremium,
isPremiumUser = organizationPremiumStatusMap[it.organizationId]
?: isPremium,
)
}
.takeIf { it.size < NO_FOLDER_ITEM_THRESHOLD }

View file

@ -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.components.model.IconData
import com.x8bit.bitwarden.ui.vault.feature.vault.model.VaultFilterType
import com.x8bit.bitwarden.ui.vault.feature.vault.util.getOrganizationPremiumStatusMap
import com.x8bit.bitwarden.ui.vault.feature.vault.util.toLoginIconData
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.combine
@ -319,8 +320,11 @@ class VerificationCodeViewModel @Inject constructor(
authCodes: List<VerificationCodeItem>,
userAccount: UserState.Account?,
): DataState<List<VerificationCodeItem>> {
val orgPremiumStatusMap = userAccount?.getOrganizationPremiumStatusMap().orEmpty()
val filteredAuthCodes = authCodes.mapNotNull { authCode ->
if (userAccount?.isPremium == true) {
val premiumStatus =
(authCode.orgId?.let { orgPremiumStatusMap[it] } ?: userAccount?.isPremium) == true
if (premiumStatus) {
authCode
} else {
authCode.takeIf { it.orgUsesTotp }

View file

@ -962,6 +962,7 @@ Do you want to switch to this account?</string>
<string name="remove_passkey">Remove passkey</string>
<string name="passkey_removed">Passkey removed</string>
<string name="what_makes_a_password_strong">What makes a password strong?</string>
<string name="a_secure_memorable_password">A secure, memorable password</string>
<string name="the_longer_your_password_the_more_difficult_to_hack">The longer your password, the more difficult it is to hack. The minimum for account creation is 12 characters but if you do 14 characters, the time to crack your password would be centuries!</string>
<string name="the_strongest_passwords_are_usually">The strongest passwords are usually:</string>
<string name="twelve_or_more_characters">12 or more characters</string>
@ -978,6 +979,7 @@ Do you want to switch to this account?</string>
<string name="set_up_biometrics_or_choose_a_pin_code_to_quickly_access_your_vault_and_autofill_your_logins">Set up biometrics or choose a PIN code to quickly access your vault and AutoFill your logins.</string>
<string name="never_lose_access_to_your_vault">Never lose access to your vault</string>
<string name="the_best_way_to_make_sure_you_can_always_access_your_account">The best way to make sure you can always access your account is to set up safeguards from the start.</string>
<string name="one_of_the_best_ways_to_create_a_secure_and_memorable_password">One of the best ways to create a secure and memorable password is to use a passphrase. \nHeres how:</string>
<string name="create_a_hint">Create a hint</string>
<string name="your_hint_will_be_send_to_you_via_email_when_you_request_it">Your hint will be sent to you via email when you request it.</string>
<string name="write_your_password_down">Write your password down</string>
@ -1099,4 +1101,15 @@ Do you want to switch to this account?</string>
<string name="biometrics_no_longer_supported">Youve been logged out because your devices biometrics dont meet the latest security requirements. To update settings, log in once again or contact your administrator for access.</string>
<string name="cxp_import">CXP Import</string>
<string name="cxp_export">CXP Export</string>
<string name="choose_three_or_four_random_words">Choose three or four random words</string>
<string name="pick_three_or_four_random_unrelated_words">Pick three or four random, unrelated words that you can easily remember. Think of objects, places, or things you like.</string>
<string name="pick_three_or_four_random_unrelated_words_highlight">objects, places, or things</string>
<string name="combine_those_words_together">Combine those words together</string>
<string name="put_the_words_together_in_any_order_to_form_your_passphrase">Put the words together in any order to form your passphrase. Use hyphens, spaces, or leave them as one long word—your choice!</string>
<string name="use_hyphens_spaces_or_leave_them_as_long_word_highlight">Use hyphens, spaces, or leave them as one long word</string>
<string name="make_it_yours">Make it yours</string>
<string name="add_a_number_or_symbol_to_make_it_even_stronger">Add a number or symbol to make it even stronger. Now you have a unique, secure, and memorable passphrase!</string>
<string name="add_a_number_or_symbol_highlight">Add a number or symbol</string>
<string name="need_some_inspiration">"Need some inspiration?"</string>
<string name="check_out_the_passphrase_generator">"Check out the passphrase generator"</string>
</resources>

View file

@ -4380,6 +4380,7 @@ class AuthRepositoryTest {
every { shouldUseKeyConnector } returns true
every { type } returns OrganizationType.USER
every { keyConnectorUrl } returns null
every { shouldUsersGetPremium } returns false
},
)
fakeAuthDiskSource.storeOrganizations(userId = USER_ID_1, organizations = organizations)
@ -4405,6 +4406,7 @@ class AuthRepositoryTest {
every { shouldUseKeyConnector } returns true
every { type } returns OrganizationType.USER
every { keyConnectorUrl } returns url
every { shouldUsersGetPremium } returns false
},
)
fakeAuthDiskSource.storeOrganizations(userId = USER_ID_1, organizations = organizations)
@ -4441,6 +4443,7 @@ class AuthRepositoryTest {
every { shouldUseKeyConnector } returns true
every { type } returns OrganizationType.USER
every { keyConnectorUrl } returns url
every { shouldUsersGetPremium } returns false
},
)
fakeAuthDiskSource.storeOrganizations(userId = USER_ID_1, organizations = organizations)

View file

@ -193,6 +193,7 @@ class AuthDiskSourceExtensionsTest {
shouldManageResetPassword = false,
shouldUseKeyConnector = false,
role = OrganizationType.ADMIN,
shouldUsersGetPremium = false,
),
),
),
@ -205,6 +206,7 @@ class AuthDiskSourceExtensionsTest {
shouldManageResetPassword = false,
shouldUseKeyConnector = false,
role = OrganizationType.ADMIN,
shouldUsersGetPremium = false,
),
),
),
@ -217,6 +219,7 @@ class AuthDiskSourceExtensionsTest {
shouldManageResetPassword = false,
shouldUseKeyConnector = false,
role = OrganizationType.ADMIN,
shouldUsersGetPremium = false,
),
),
),
@ -364,6 +367,7 @@ class AuthDiskSourceExtensionsTest {
shouldManageResetPassword = false,
shouldUseKeyConnector = false,
role = OrganizationType.ADMIN,
shouldUsersGetPremium = false,
),
),
),
@ -395,6 +399,7 @@ class AuthDiskSourceExtensionsTest {
shouldManageResetPassword = false,
shouldUseKeyConnector = false,
role = OrganizationType.ADMIN,
shouldUsersGetPremium = false,
),
),
),
@ -407,6 +412,7 @@ class AuthDiskSourceExtensionsTest {
shouldManageResetPassword = false,
shouldUseKeyConnector = false,
role = OrganizationType.ADMIN,
shouldUsersGetPremium = false,
),
),
),

View file

@ -23,6 +23,7 @@ class SyncResponseJsonExtensionsTest {
shouldManageResetPassword = false,
shouldUseKeyConnector = false,
role = OrganizationType.ADMIN,
shouldUsersGetPremium = false,
),
createMockOrganization(number = 1).toOrganization(),
)
@ -38,6 +39,7 @@ class SyncResponseJsonExtensionsTest {
shouldManageResetPassword = false,
shouldUseKeyConnector = true,
role = OrganizationType.ADMIN,
shouldUsersGetPremium = false,
),
Organization(
id = "mockId-2",
@ -45,6 +47,7 @@ class SyncResponseJsonExtensionsTest {
shouldManageResetPassword = true,
shouldUseKeyConnector = false,
role = OrganizationType.USER,
shouldUsersGetPremium = false,
),
),
listOf(

View file

@ -350,6 +350,7 @@ class UserStateJsonExtensionsTest {
shouldManageResetPassword = false,
shouldUseKeyConnector = false,
role = OrganizationType.ADMIN,
shouldUsersGetPremium = false,
),
),
isBiometricsEnabled = false,
@ -414,6 +415,7 @@ class UserStateJsonExtensionsTest {
shouldManageResetPassword = false,
shouldUseKeyConnector = false,
role = OrganizationType.ADMIN,
shouldUsersGetPremium = false,
),
),
),
@ -458,6 +460,7 @@ class UserStateJsonExtensionsTest {
shouldManageResetPassword = false,
shouldUseKeyConnector = false,
role = OrganizationType.ADMIN,
shouldUsersGetPremium = false,
),
),
isBiometricsEnabled = true,
@ -518,6 +521,7 @@ class UserStateJsonExtensionsTest {
shouldManageResetPassword = false,
shouldUseKeyConnector = false,
role = OrganizationType.ADMIN,
shouldUsersGetPremium = false,
),
),
),
@ -563,6 +567,7 @@ class UserStateJsonExtensionsTest {
shouldManageResetPassword = false,
shouldUseKeyConnector = false,
role = OrganizationType.ADMIN,
shouldUsersGetPremium = false,
),
),
isBiometricsEnabled = false,
@ -631,6 +636,7 @@ class UserStateJsonExtensionsTest {
shouldManageResetPassword = false,
shouldUseKeyConnector = false,
role = OrganizationType.ADMIN,
shouldUsersGetPremium = false,
),
),
),
@ -676,6 +682,7 @@ class UserStateJsonExtensionsTest {
shouldManageResetPassword = false,
shouldUseKeyConnector = false,
role = OrganizationType.ADMIN,
shouldUsersGetPremium = false,
),
),
isBiometricsEnabled = false,
@ -744,6 +751,7 @@ class UserStateJsonExtensionsTest {
shouldManageResetPassword = false,
shouldUseKeyConnector = false,
role = OrganizationType.ADMIN,
shouldUsersGetPremium = false,
),
),
),
@ -789,6 +797,7 @@ class UserStateJsonExtensionsTest {
shouldManageResetPassword = false,
shouldUseKeyConnector = false,
role = OrganizationType.ADMIN,
shouldUsersGetPremium = false,
),
),
isBiometricsEnabled = false,
@ -857,6 +866,7 @@ class UserStateJsonExtensionsTest {
shouldManageResetPassword = false,
shouldUseKeyConnector = false,
role = OrganizationType.ADMIN,
shouldUsersGetPremium = false,
),
),
),
@ -903,6 +913,7 @@ class UserStateJsonExtensionsTest {
shouldManageResetPassword = true,
shouldUseKeyConnector = false,
role = OrganizationType.USER,
shouldUsersGetPremium = false,
),
),
isBiometricsEnabled = false,
@ -974,6 +985,7 @@ class UserStateJsonExtensionsTest {
shouldManageResetPassword = true,
shouldUseKeyConnector = false,
role = OrganizationType.USER,
shouldUsersGetPremium = false,
),
),
),
@ -1178,6 +1190,7 @@ class UserStateJsonExtensionsTest {
shouldManageResetPassword = false,
shouldUseKeyConnector = false,
role = OrganizationType.USER,
shouldUsersGetPremium = false,
),
),
isBiometricsEnabled = false,
@ -1248,6 +1261,7 @@ class UserStateJsonExtensionsTest {
shouldManageResetPassword = false,
shouldUseKeyConnector = false,
role = OrganizationType.USER,
shouldUsersGetPremium = false,
),
),
),
@ -1293,6 +1307,7 @@ class UserStateJsonExtensionsTest {
shouldManageResetPassword = false,
shouldUseKeyConnector = false,
role = OrganizationType.ADMIN,
shouldUsersGetPremium = false,
),
),
isBiometricsEnabled = false,
@ -1363,6 +1378,7 @@ class UserStateJsonExtensionsTest {
shouldManageResetPassword = false,
shouldUseKeyConnector = false,
role = OrganizationType.ADMIN,
shouldUsersGetPremium = false,
),
),
),

View file

@ -6,6 +6,7 @@ import com.bitwarden.vault.CipherView
import com.bitwarden.vault.LoginView
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.model.Organization
import com.x8bit.bitwarden.data.auth.repository.model.UserState
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
@ -94,8 +95,10 @@ class AutofillTotpManagerTest {
runTest {
every { settingsRepository.isAutoCopyTotpDisabled } returns false
every { cipherView.organizationUseTotp } returns false
every { cipherView.organizationId } returns null
mutableUserStateFlow.value = mockk {
every { activeAccount.isPremium } returns false
every { activeAccount.organizations } returns emptyList()
}
autofillTotpManager.tryCopyTotpToClipboard(cipherView = cipherView)
@ -115,8 +118,10 @@ class AutofillTotpManagerTest {
runTest {
every { settingsRepository.isAutoCopyTotpDisabled } returns false
every { cipherView.organizationUseTotp } returns true
every { cipherView.organizationId } returns null
mutableUserStateFlow.value = mockk {
every { activeAccount.isPremium } returns true
every { activeAccount.organizations } returns emptyList()
}
every { loginView.totp } returns null
@ -141,8 +146,10 @@ class AutofillTotpManagerTest {
)
every { settingsRepository.isAutoCopyTotpDisabled } returns false
every { cipherView.organizationUseTotp } returns true
every { cipherView.organizationId } returns null
mutableUserStateFlow.value = mockk {
every { activeAccount.isPremium } returns true
every { activeAccount.organizations } returns emptyList()
}
every { loginView.totp } returns TOTP_CODE
coEvery {
@ -160,6 +167,34 @@ class AutofillTotpManagerTest {
vaultRepository.generateTotp(time = FIXED_CLOCK.instant(), totpCode = TOTP_CODE)
}
}
@Suppress("MaxLineLength")
@Test
fun `tryCopyTotpToClipboard when isAutoCopyTotpDisabled is false, user has premium but item belongs to an org that doesn't should do nothing`() =
runTest {
val orgId = "orgId"
val mockOrganization = mockk<Organization>(relaxed = true) {
every { id } returns orgId
every { shouldUsersGetPremium } returns false
}
every { settingsRepository.isAutoCopyTotpDisabled } returns false
every { cipherView.organizationUseTotp } returns false
every { cipherView.organizationId } returns orgId
mutableUserStateFlow.value = mockk {
every { activeAccount.isPremium } returns true
every { activeAccount.organizations } returns listOf(mockOrganization)
}
autofillTotpManager.tryCopyTotpToClipboard(cipherView = cipherView)
verify(exactly = 0) {
clipboardManager.setText(text = TOTP_RESULT_VALUE)
toast.show()
}
coVerify(exactly = 0) {
vaultRepository.generateTotp(time = FIXED_CLOCK.instant(), totpCode = TOTP_CODE)
}
}
}
private val FIXED_CLOCK: Clock = Clock.fixed(

View file

@ -49,10 +49,12 @@ fun createMockCipherView(
clock: Clock = FIXED_CLOCK,
fido2Credentials: List<Fido2Credential>? = null,
sshKey: SshKeyView? = createMockSshKeyView(number = number),
organizationUsesTotp: Boolean = false,
organizationId: String? = "mockOrganizationId-$number",
): CipherView =
CipherView(
id = "mockId-$number",
organizationId = "mockOrganizationId-$number",
organizationId = organizationId,
folderId = folderId,
collectionIds = listOf("mockId-$number"),
key = "mockKey-$number",
@ -85,7 +87,7 @@ fun createMockCipherView(
reprompt = repromptType,
secureNote = createMockSecureNoteView().takeIf { cipherType == CipherType.SECURE_NOTE },
edit = true,
organizationUseTotp = false,
organizationUseTotp = organizationUsesTotp,
viewPassword = true,
localData = null,
)

View file

@ -48,7 +48,7 @@ class TotpCodeManagerTest {
vaultSdkSource.generateTotp(any(), any(), any())
} returns totpResponse.asSuccess()
val expected = createVerificationCodeItem()
val expected = createVerificationCodeItem().copy(orgId = "mockOrganizationId-1")
totpCodeManager.getTotpCodesStateFlow(userId, cipherList).test {
assertEquals(DataState.Loaded(listOf(expected)), awaitItem())
@ -106,7 +106,10 @@ class TotpCodeManagerTest {
repromptType = CipherRepromptType.PASSWORD,
)
val expected = createVerificationCodeItem().copy(hasPasswordReprompt = true)
val expected = createVerificationCodeItem().copy(
hasPasswordReprompt = true,
orgId = cipherView.organizationId,
)
totpCodeManager.getTotpCodeStateFlow(userId, cipherView).test {
assertEquals(DataState.Loaded(expected), awaitItem())

View file

@ -3,6 +3,7 @@ package com.x8bit.bitwarden.ui.auth.feature.masterpasswordguidance
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performScrollTo
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
import io.mockk.every
@ -45,7 +46,8 @@ class MasterPasswordGuidanceScreenTest : BaseComposeTest() {
@Test
fun `Generator card click should invoke send of TryPasswordGeneratorAction`() {
composeTestRule
.onNodeWithText("Use the generator to create a strong, unique password")
.onNodeWithText("Check out the passphrase generator")
.performScrollTo()
.performClick()
verify { viewModel.trySendAction(MasterPasswordGuidanceAction.TryPasswordGeneratorAction) }

View file

@ -161,6 +161,7 @@ private val DEFAULT_ACCOUNT = UserState.Account(
shouldManageResetPassword = false,
shouldUseKeyConnector = true,
role = OrganizationType.USER,
shouldUsersGetPremium = false,
),
),
needsMasterPassword = false,

View file

@ -386,6 +386,7 @@ class RootNavViewModelTest : BaseViewModelTest() {
shouldManageResetPassword = false,
shouldUseKeyConnector = true,
role = OrganizationType.USER,
shouldUsersGetPremium = false,
),
),
needsMasterPassword = false,

View file

@ -938,6 +938,7 @@ private val DEFAULT_STATE: SearchState = SearchState(
totpData = null,
autofillSelectionData = null,
isPremium = true,
organizationPremiumStatusMap = emptyMap(),
)
private fun createStateForAutofill(

View file

@ -1016,6 +1016,7 @@ class SearchViewModelTest : BaseViewModelTest() {
hasMasterPassword = true,
isPremiumUser = true,
isTotp = false,
organizationPremiumStatusMap = emptyMap(),
)
} returns expectedViewState
val dataState = DataState.Loaded(
@ -1119,6 +1120,7 @@ class SearchViewModelTest : BaseViewModelTest() {
hasMasterPassword = true,
isPremiumUser = true,
isTotp = false,
organizationPremiumStatusMap = emptyMap(),
)
} returns expectedViewState
mutableVaultDataStateFlow.tryEmit(
@ -1232,6 +1234,7 @@ class SearchViewModelTest : BaseViewModelTest() {
hasMasterPassword = true,
isPremiumUser = true,
isTotp = false,
organizationPremiumStatusMap = emptyMap(),
)
} returns expectedViewState
val dataState = DataState.Error(
@ -1348,6 +1351,7 @@ class SearchViewModelTest : BaseViewModelTest() {
hasMasterPassword = true,
isPremiumUser = true,
isTotp = false,
organizationPremiumStatusMap = emptyMap(),
)
} returns expectedViewState
val dataState = DataState.NoNetwork(
@ -1526,6 +1530,7 @@ class SearchViewModelTest : BaseViewModelTest() {
hasMasterPassword = true,
isPremiumUser = true,
isTotp = false,
organizationPremiumStatusMap = emptyMap(),
)
} returns expectedViewState
val dataState = DataState.Loaded(
@ -1561,6 +1566,7 @@ private val DEFAULT_STATE: SearchState = SearchState(
totpData = null,
autofillSelectionData = null,
isPremium = true,
organizationPremiumStatusMap = emptyMap(),
)
private val DEFAULT_USER_STATE = UserState(

View file

@ -326,6 +326,7 @@ class SearchTypeDataExtensionsTest {
hasMasterPassword = true,
isPremiumUser = true,
isTotp = true,
organizationPremiumStatusMap = emptyMap(),
)
assertEquals(SearchState.ViewState.Empty(message = null), result)
@ -352,6 +353,7 @@ class SearchTypeDataExtensionsTest {
hasMasterPassword = true,
isPremiumUser = true,
isTotp = false,
organizationPremiumStatusMap = emptyMap(),
)
assertEquals(
@ -393,6 +395,7 @@ class SearchTypeDataExtensionsTest {
hasMasterPassword = true,
isPremiumUser = true,
isTotp = false,
organizationPremiumStatusMap = emptyMap(),
)
assertEquals(
@ -444,6 +447,7 @@ class SearchTypeDataExtensionsTest {
hasMasterPassword = true,
isPremiumUser = true,
isTotp = true,
organizationPremiumStatusMap = emptyMap(),
)
assertEquals(

View file

@ -4396,6 +4396,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
shouldManageResetPassword = false,
shouldUseKeyConnector = false,
role = OrganizationType.ADMIN,
shouldUsersGetPremium = false,
),
),
isBiometricsEnabled = true,

View file

@ -565,6 +565,7 @@ class CipherViewExtensionsTest {
shouldManageResetPassword = false,
shouldUseKeyConnector = false,
role = OrganizationType.ADMIN,
shouldUsersGetPremium = false,
),
),
isBiometricsEnabled = true,

View file

@ -1366,6 +1366,42 @@ class VaultItemScreenTest : BaseComposeTest() {
viewModel.trySendAction(VaultItemAction.Common.CopyNotesClick)
}
}
@Test
fun `on ssh key copy notes field click should send CopyNotesClick`() {
// Adding a custom field so that we can scroll to it
// So we can see the Copy notes button but not have it covered by the FAB
val textField = VaultItemState.ViewState.Content.Common.Custom.TextField(
name = "text",
value = "value",
isCopyable = true,
)
EMPTY_VIEW_STATES
.forEach { typeState ->
mutableStateFlow.update { currentState ->
currentState.copy(
viewState = typeState.copy(
type = DEFAULT_SSH_KEY,
common = EMPTY_COMMON.copy(
notes = "this is a note",
customFields = listOf(textField),
),
),
)
}
}
composeTestRule.onNodeWithTextAfterScroll(textField.name)
composeTestRule
.onNodeWithTag("CipherNotesCopyButton")
.performClick()
verify {
viewModel.trySendAction(VaultItemAction.Common.CopyNotesClick)
}
}
//endregion common
//region login
@ -2602,29 +2638,6 @@ private fun updateCardType(
return currentState.copy(viewState = updatedType)
}
private fun updateSshKeyType(
currentState: VaultItemState,
transform: VaultItemState.ViewState.Content.ItemType.SshKey.() ->
VaultItemState.ViewState.Content.ItemType.SshKey,
): VaultItemState {
val updatedType = when (val viewState = currentState.viewState) {
is VaultItemState.ViewState.Content -> {
when (val type = viewState.type) {
is VaultItemState.ViewState.Content.ItemType.SshKey -> {
viewState.copy(
type = type.transform(),
)
}
else -> viewState
}
}
else -> viewState
}
return currentState.copy(viewState = updatedType)
}
private fun updateCommonContent(
currentState: VaultItemState,
transform: VaultItemState.ViewState.Content.Common.()
@ -2811,6 +2824,15 @@ private val EMPTY_CARD_TYPE: VaultItemState.ViewState.Content.ItemType.Card =
),
)
private val EMPTY_SSH_KEY_TYPE: VaultItemState.ViewState.Content.ItemType.SshKey =
VaultItemState.ViewState.Content.ItemType.SshKey(
name = "",
publicKey = "",
privateKey = "",
fingerprint = "",
showPrivateKey = false,
)
private val EMPTY_LOGIN_VIEW_STATE: VaultItemState.ViewState.Content =
VaultItemState.ViewState.Content(
common = EMPTY_COMMON,
@ -2835,6 +2857,12 @@ private val EMPTY_SECURE_NOTE_VIEW_STATE =
type = VaultItemState.ViewState.Content.ItemType.SecureNote,
)
private val EMPTY_SSH_KEY_VIEW_STATE =
VaultItemState.ViewState.Content(
common = EMPTY_COMMON,
type = EMPTY_SSH_KEY_TYPE,
)
private val DEFAULT_LOGIN_VIEW_STATE: VaultItemState.ViewState.Content =
VaultItemState.ViewState.Content(
type = DEFAULT_LOGIN,
@ -2869,10 +2897,12 @@ private val EMPTY_VIEW_STATES = listOf(
EMPTY_LOGIN_VIEW_STATE,
EMPTY_IDENTITY_VIEW_STATE,
EMPTY_SECURE_NOTE_VIEW_STATE,
EMPTY_SSH_KEY_VIEW_STATE,
)
private val DEFAULT_VIEW_STATES = listOf(
DEFAULT_LOGIN_VIEW_STATE,
DEFAULT_IDENTITY_VIEW_STATE,
DEFAULT_SECURE_NOTE_VIEW_STATE,
DEFAULT_SSH_KEY_VIEW_STATE,
)

View file

@ -2159,6 +2159,7 @@ private val DEFAULT_STATE = VaultItemListingState(
hasMasterPassword = true,
isPremium = false,
isRefreshing = false,
organizationPremiumStatusMap = emptyMap(),
)
private val STATE_FOR_AUTOFILL = DEFAULT_STATE.copy(

View file

@ -4079,6 +4079,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
fido2CreateCredentialRequest = null,
isPremium = true,
isRefreshing = false,
organizationPremiumStatusMap = emptyMap(),
)
}

View file

@ -464,6 +464,7 @@ class VaultItemListingDataExtensionsTest {
fido2CredentialAutofillViews = null,
totpData = null,
isPremiumUser = true,
organizationPremiumStatusMap = emptyMap(),
)
assertEquals(
@ -557,6 +558,7 @@ class VaultItemListingDataExtensionsTest {
fido2CredentialAutofillViews = fido2CredentialAutofillViews,
totpData = null,
isPremiumUser = true,
organizationPremiumStatusMap = emptyMap(),
)
assertEquals(
@ -643,6 +645,7 @@ class VaultItemListingDataExtensionsTest {
fido2CredentialAutofillViews = fido2CredentialAutofillViews,
totpData = null,
isPremiumUser = true,
organizationPremiumStatusMap = emptyMap(),
)
assertEquals(
@ -702,6 +705,7 @@ class VaultItemListingDataExtensionsTest {
fido2CredentialAutofillViews = null,
totpData = null,
isPremiumUser = true,
organizationPremiumStatusMap = emptyMap(),
),
)
@ -726,6 +730,7 @@ class VaultItemListingDataExtensionsTest {
fido2CredentialAutofillViews = null,
totpData = null,
isPremiumUser = true,
organizationPremiumStatusMap = emptyMap(),
),
)
@ -748,6 +753,7 @@ class VaultItemListingDataExtensionsTest {
fido2CredentialAutofillViews = null,
totpData = null,
isPremiumUser = true,
organizationPremiumStatusMap = emptyMap(),
),
)
@ -770,6 +776,7 @@ class VaultItemListingDataExtensionsTest {
fido2CredentialAutofillViews = null,
totpData = null,
isPremiumUser = true,
organizationPremiumStatusMap = emptyMap(),
),
)
@ -796,6 +803,7 @@ class VaultItemListingDataExtensionsTest {
fido2CredentialAutofillViews = null,
totpData = null,
isPremiumUser = true,
organizationPremiumStatusMap = emptyMap(),
),
)
@ -824,6 +832,7 @@ class VaultItemListingDataExtensionsTest {
fido2CredentialAutofillViews = null,
totpData = null,
isPremiumUser = true,
organizationPremiumStatusMap = emptyMap(),
),
)
@ -850,6 +859,7 @@ class VaultItemListingDataExtensionsTest {
every { issuer } returns "issuer"
},
isPremiumUser = true,
organizationPremiumStatusMap = emptyMap(),
),
)
}
@ -1054,6 +1064,7 @@ class VaultItemListingDataExtensionsTest {
fido2CredentialAutofillViews = null,
totpData = null,
isPremiumUser = true,
organizationPremiumStatusMap = emptyMap(),
)
assertEquals(
@ -1098,6 +1109,7 @@ class VaultItemListingDataExtensionsTest {
fido2CredentialAutofillViews = null,
totpData = null,
isPremiumUser = true,
organizationPremiumStatusMap = emptyMap(),
)
assertEquals(

View file

@ -496,6 +496,7 @@ private val DEFAULT_USER_STATE = UserState(
shouldManageResetPassword = false,
shouldUseKeyConnector = false,
role = OrganizationType.ADMIN,
shouldUsersGetPremium = false,
),
Organization(
id = "mockOrganizationId-2",
@ -503,6 +504,7 @@ private val DEFAULT_USER_STATE = UserState(
shouldManageResetPassword = false,
shouldUseKeyConnector = false,
role = OrganizationType.ADMIN,
shouldUsersGetPremium = false,
),
Organization(
id = "mockOrganizationId-3",
@ -510,6 +512,7 @@ private val DEFAULT_USER_STATE = UserState(
shouldManageResetPassword = false,
shouldUseKeyConnector = false,
role = OrganizationType.ADMIN,
shouldUsersGetPremium = false,
),
),
trustedDevice = null,

View file

@ -108,6 +108,7 @@ private fun createMockUserState(hasOrganizations: Boolean = true): UserState =
shouldManageResetPassword = false,
shouldUseKeyConnector = false,
role = OrganizationType.ADMIN,
shouldUsersGetPremium = false,
),
Organization(
id = "mockOrganizationId-2",
@ -115,6 +116,7 @@ private fun createMockUserState(hasOrganizations: Boolean = true): UserState =
shouldManageResetPassword = false,
shouldUseKeyConnector = false,
role = OrganizationType.ADMIN,
shouldUsersGetPremium = false,
),
Organization(
id = "mockOrganizationId-3",
@ -122,6 +124,7 @@ private fun createMockUserState(hasOrganizations: Boolean = true): UserState =
shouldManageResetPassword = false,
shouldUseKeyConnector = false,
role = OrganizationType.ADMIN,
shouldUsersGetPremium = false,
),
)
} else {

View file

@ -1329,6 +1329,7 @@ private val DEFAULT_STATE: VaultState = VaultState(
isRefreshing = false,
showImportActionCard = false,
showSshKeys = false,
organizationPremiumStatusMap = emptyMap(),
)
private val DEFAULT_CONTENT_VIEW_STATE: VaultState.ViewState.Content = VaultState.ViewState.Content(

View file

@ -230,6 +230,7 @@ class VaultViewModelTest : BaseViewModelTest() {
shouldManageResetPassword = false,
shouldUseKeyConnector = false,
role = OrganizationType.ADMIN,
shouldUsersGetPremium = false,
),
),
trustedDevice = null,
@ -316,6 +317,7 @@ class VaultViewModelTest : BaseViewModelTest() {
shouldManageResetPassword = false,
shouldUseKeyConnector = false,
role = OrganizationType.ADMIN,
shouldUsersGetPremium = false,
),
),
trustedDevice = null,
@ -526,6 +528,7 @@ class VaultViewModelTest : BaseViewModelTest() {
shouldManageResetPassword = false,
shouldUseKeyConnector = false,
role = OrganizationType.ADMIN,
shouldUsersGetPremium = true,
),
),
),
@ -541,7 +544,9 @@ class VaultViewModelTest : BaseViewModelTest() {
baseIconUrl = viewModel.stateFlow.value.baseIconUrl,
hasMasterPassword = true,
showSshKeys = false,
organizationPremiumStatusMap = mapOf("testOrganizationId" to true),
),
organizationPremiumStatusMap = mapOf("testOrganizationId" to true),
)
.copy(
appBarTitle = R.string.vaults.asText(),
@ -554,20 +559,22 @@ class VaultViewModelTest : BaseViewModelTest() {
viewModel.trySendAction(VaultAction.VaultFilterTypeSelect(VaultFilterType.MyVault))
assertEquals(
initialState.copy(
vaultFilterData = VAULT_FILTER_DATA.copy(
selectedVaultFilterType = VaultFilterType.MyVault,
),
viewState = vaultData.toViewState(
isPremium = true,
vaultFilterType = VaultFilterType.MyVault,
isIconLoadingDisabled = viewModel.stateFlow.value.isIconLoadingDisabled,
baseIconUrl = viewModel.stateFlow.value.baseIconUrl,
hasMasterPassword = true,
showSshKeys = false,
),
val resultingState = initialState.copy(
vaultFilterData = VAULT_FILTER_DATA.copy(
selectedVaultFilterType = VaultFilterType.MyVault,
),
viewState = vaultData.toViewState(
isPremium = true,
vaultFilterType = VaultFilterType.MyVault,
isIconLoadingDisabled = viewModel.stateFlow.value.isIconLoadingDisabled,
baseIconUrl = viewModel.stateFlow.value.baseIconUrl,
hasMasterPassword = true,
showSshKeys = false,
organizationPremiumStatusMap = mapOf("testOrganizationId" to true),
),
)
assertEquals(
resultingState,
viewModel.stateFlow.value,
)
verify { vaultRepository.vaultFilterType = VaultFilterType.MyVault }
@ -671,7 +678,7 @@ class VaultViewModelTest : BaseViewModelTest() {
),
noFolderItems = listOf(),
trashItemsCount = 0,
totpItemsCount = 1,
totpItemsCount = 0,
itemTypesCount = CipherType.entries.size,
sshKeyItemsCount = 1,
),
@ -696,7 +703,7 @@ class VaultViewModelTest : BaseViewModelTest() {
collectionItems = listOf(),
noFolderItems = listOf(),
trashItemsCount = 0,
totpItemsCount = 1,
totpItemsCount = 0,
itemTypesCount = 4,
sshKeyItemsCount = 0,
),
@ -810,7 +817,7 @@ class VaultViewModelTest : BaseViewModelTest() {
),
noFolderItems = listOf(),
trashItemsCount = 0,
totpItemsCount = 1,
totpItemsCount = 0,
itemTypesCount = 4,
sshKeyItemsCount = 0,
),
@ -910,7 +917,7 @@ class VaultViewModelTest : BaseViewModelTest() {
),
noFolderItems = listOf(),
trashItemsCount = 0,
totpItemsCount = 1,
totpItemsCount = 0,
itemTypesCount = 4,
sshKeyItemsCount = 0,
),
@ -976,7 +983,12 @@ class VaultViewModelTest : BaseViewModelTest() {
mutableVaultDataStateFlow.tryEmit(
value = DataState.NoNetwork(
data = VaultData(
cipherViewList = listOf(createMockCipherView(number = 1)),
cipherViewList = listOf(
createMockCipherView(
number = 1,
organizationUsesTotp = true,
),
),
collectionViewList = listOf(createMockCollectionView(number = 1)),
folderViewList = listOf(createMockFolderView(number = 1)),
sendViewList = listOf(createMockSendView(number = 1)),
@ -1116,7 +1128,7 @@ class VaultViewModelTest : BaseViewModelTest() {
collectionItems = listOf(),
noFolderItems = listOf(),
trashItemsCount = 0,
totpItemsCount = 1,
totpItemsCount = 0,
itemTypesCount = CipherType.entries.size - 1,
sshKeyItemsCount = 0,
),
@ -1157,7 +1169,7 @@ class VaultViewModelTest : BaseViewModelTest() {
collectionItems = listOf(),
noFolderItems = listOf(),
trashItemsCount = 0,
totpItemsCount = 1,
totpItemsCount = 0,
itemTypesCount = CipherType.entries.size,
sshKeyItemsCount = 1,
),
@ -1915,6 +1927,7 @@ private fun createMockVaultState(
viewState: VaultState.ViewState,
dialog: VaultState.DialogState? = null,
showSshKeys: Boolean = false,
organizationPremiumStatusMap: Map<String, Boolean> = emptyMap(),
): VaultState =
VaultState(
appBarTitle = R.string.my_vault.asText(),
@ -1953,4 +1966,5 @@ private fun createMockVaultState(
showImportActionCard = true,
isRefreshing = false,
showSshKeys = showSshKeys,
organizationPremiumStatusMap = organizationPremiumStatusMap,
)

View file

@ -82,6 +82,7 @@ class UserStateExtensionsTest {
shouldManageResetPassword = false,
shouldUseKeyConnector = false,
role = OrganizationType.ADMIN,
shouldUsersGetPremium = false,
),
),
trustedDevice = null,
@ -109,6 +110,7 @@ class UserStateExtensionsTest {
shouldManageResetPassword = false,
shouldUseKeyConnector = false,
role = OrganizationType.ADMIN,
shouldUsersGetPremium = false,
),
),
trustedDevice = null,
@ -140,6 +142,7 @@ class UserStateExtensionsTest {
shouldManageResetPassword = false,
shouldUseKeyConnector = false,
role = OrganizationType.ADMIN,
shouldUsersGetPremium = false,
),
),
trustedDevice = null,
@ -171,6 +174,7 @@ class UserStateExtensionsTest {
shouldManageResetPassword = false,
shouldUseKeyConnector = false,
role = OrganizationType.ADMIN,
shouldUsersGetPremium = false,
),
),
trustedDevice = null,
@ -217,6 +221,7 @@ class UserStateExtensionsTest {
shouldManageResetPassword = false,
shouldUseKeyConnector = false,
role = OrganizationType.ADMIN,
shouldUsersGetPremium = false,
),
),
trustedDevice = null,
@ -261,6 +266,7 @@ class UserStateExtensionsTest {
shouldManageResetPassword = false,
shouldUseKeyConnector = false,
role = OrganizationType.ADMIN,
shouldUsersGetPremium = false,
),
),
trustedDevice = null,
@ -309,6 +315,7 @@ class UserStateExtensionsTest {
shouldManageResetPassword = false,
shouldUseKeyConnector = false,
role = OrganizationType.ADMIN,
shouldUsersGetPremium = false,
),
),
trustedDevice = null,
@ -387,6 +394,7 @@ class UserStateExtensionsTest {
shouldUseKeyConnector = false,
shouldManageResetPassword = false,
role = OrganizationType.ADMIN,
shouldUsersGetPremium = false,
),
Organization(
id = "organizationId-A",
@ -394,6 +402,7 @@ class UserStateExtensionsTest {
shouldManageResetPassword = false,
shouldUseKeyConnector = false,
role = OrganizationType.ADMIN,
shouldUsersGetPremium = false,
),
),
trustedDevice = null,
@ -445,6 +454,7 @@ class UserStateExtensionsTest {
shouldManageResetPassword = false,
shouldUseKeyConnector = false,
role = OrganizationType.ADMIN,
shouldUsersGetPremium = false,
),
Organization(
id = "organizationId-A",
@ -452,6 +462,7 @@ class UserStateExtensionsTest {
shouldManageResetPassword = false,
shouldUseKeyConnector = false,
role = OrganizationType.ADMIN,
shouldUsersGetPremium = false,
),
),
trustedDevice = null,
@ -465,4 +476,51 @@ class UserStateExtensionsTest {
),
)
}
@Test
fun `getOrganizationPremiumStatusMap should map organizations to correct status`() {
val actualMap = UserState.Account(
userId = "userId",
name = "name",
email = "email",
avatarColorHex = "avatarColorHex",
environment = Environment.Us,
isPremium = false,
isLoggedIn = true,
isVaultUnlocked = false,
needsPasswordReset = false,
isBiometricsEnabled = false,
needsMasterPassword = false,
organizations = listOf(
Organization(
id = "1",
name = "organizationName",
shouldManageResetPassword = false,
shouldUseKeyConnector = false,
role = OrganizationType.ADMIN,
shouldUsersGetPremium = false,
), Organization(
id = "2",
name = "organizationName",
shouldManageResetPassword = false,
shouldUseKeyConnector = false,
role = OrganizationType.ADMIN,
shouldUsersGetPremium = true,
),
),
trustedDevice = null,
hasMasterPassword = true,
isUsingKeyConnector = false,
onboardingStatus = OnboardingStatus.COMPLETE,
firstTimeState = FirstTimeState(showImportLoginsCard = true),
).getOrganizationPremiumStatusMap()
assertEquals(
mapOf(
"1" to false,
"2" to true,
),
actualMap,
)
}
}

View file

@ -62,6 +62,7 @@ class VaultDataExtensionsTest {
vaultFilterType = VaultFilterType.AllVaults,
hasMasterPassword = true,
showSshKeys = false,
organizationPremiumStatusMap = emptyMap(),
)
assertEquals(
@ -98,7 +99,7 @@ class VaultDataExtensionsTest {
),
noFolderItems = listOf(),
trashItemsCount = 0,
totpItemsCount = 1,
totpItemsCount = 0,
itemTypesCount = 4,
sshKeyItemsCount = 0,
),
@ -126,6 +127,7 @@ class VaultDataExtensionsTest {
vaultFilterType = VaultFilterType.MyVault,
hasMasterPassword = true,
showSshKeys = false,
organizationPremiumStatusMap = emptyMap(),
)
assertEquals(
@ -182,6 +184,7 @@ class VaultDataExtensionsTest {
),
hasMasterPassword = true,
showSshKeys = false,
organizationPremiumStatusMap = emptyMap(),
)
assertEquals(
@ -207,7 +210,7 @@ class VaultDataExtensionsTest {
),
noFolderItems = listOf(),
trashItemsCount = 0,
totpItemsCount = 1,
totpItemsCount = 0,
itemTypesCount = 4,
sshKeyItemsCount = 0,
),
@ -231,6 +234,7 @@ class VaultDataExtensionsTest {
vaultFilterType = VaultFilterType.AllVaults,
hasMasterPassword = true,
showSshKeys = false,
organizationPremiumStatusMap = emptyMap(),
)
assertEquals(
@ -255,6 +259,7 @@ class VaultDataExtensionsTest {
vaultFilterType = VaultFilterType.AllVaults,
hasMasterPassword = true,
showSshKeys = false,
organizationPremiumStatusMap = emptyMap(),
)
assertEquals(
@ -265,9 +270,9 @@ class VaultDataExtensionsTest {
@Suppress("MaxLineLength")
@Test
fun `toViewState should return 1 for totpItemsCount if user has premium and has one totp item`() {
fun `toViewState should return 1 for totpItemsCount if user has premium and has one totp item and item is owned by user`() {
val vaultData = VaultData(
cipherViewList = listOf(createMockCipherView(number = 1)),
cipherViewList = listOf(createMockCipherView(number = 1, organizationId = null)),
collectionViewList = listOf(),
folderViewList = listOf(),
sendViewList = listOf(),
@ -280,6 +285,7 @@ class VaultDataExtensionsTest {
vaultFilterType = VaultFilterType.AllVaults,
hasMasterPassword = true,
showSshKeys = false,
organizationPremiumStatusMap = emptyMap(),
)
assertEquals(
@ -318,6 +324,7 @@ class VaultDataExtensionsTest {
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
hasMasterPassword = true,
showSshKeys = false,
organizationPremiumStatusMap = emptyMap(),
)
assertEquals(
@ -356,6 +363,7 @@ class VaultDataExtensionsTest {
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
hasMasterPassword = true,
showSshKeys = false,
organizationPremiumStatusMap = emptyMap(),
)
assertEquals(
@ -396,6 +404,7 @@ class VaultDataExtensionsTest {
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
hasMasterPassword = true,
showSshKeys = false,
organizationPremiumStatusMap = emptyMap(),
)
assertEquals(
@ -620,6 +629,7 @@ class VaultDataExtensionsTest {
vaultFilterType = VaultFilterType.AllVaults,
hasMasterPassword = true,
showSshKeys = false,
organizationPremiumStatusMap = emptyMap(),
)
assertEquals(
@ -633,7 +643,7 @@ class VaultDataExtensionsTest {
collectionItems = listOf(),
noFolderItems = listOf(),
trashItemsCount = 2,
totpItemsCount = 1,
totpItemsCount = 0,
itemTypesCount = 4,
sshKeyItemsCount = 0,
),
@ -660,6 +670,7 @@ class VaultDataExtensionsTest {
vaultFilterType = VaultFilterType.AllVaults,
hasMasterPassword = true,
showSshKeys = false,
organizationPremiumStatusMap = emptyMap(),
)
assertEquals(
@ -689,7 +700,7 @@ class VaultDataExtensionsTest {
every { uriMock.host } returns "www.mockuri1.com"
val vaultData = VaultData(
cipherViewList = List(100) {
createMockCipherView(number = it, folderId = null)
createMockCipherView(number = it, folderId = null, organizationUsesTotp = true)
},
collectionViewList = listOf(),
folderViewList = listOf(),
@ -703,6 +714,7 @@ class VaultDataExtensionsTest {
vaultFilterType = VaultFilterType.AllVaults,
hasMasterPassword = true,
showSshKeys = false,
organizationPremiumStatusMap = emptyMap(),
)
assertEquals(
@ -759,6 +771,7 @@ class VaultDataExtensionsTest {
vaultFilterType = VaultFilterType.AllVaults,
hasMasterPassword = true,
showSshKeys = false,
organizationPremiumStatusMap = emptyMap(),
)
assertEquals(
@ -800,7 +813,7 @@ class VaultDataExtensionsTest {
),
noFolderItems = listOf(),
trashItemsCount = 0,
totpItemsCount = 1,
totpItemsCount = 0,
itemTypesCount = 4,
sshKeyItemsCount = 0,
),
@ -828,6 +841,7 @@ class VaultDataExtensionsTest {
vaultFilterType = VaultFilterType.AllVaults,
hasMasterPassword = true,
showSshKeys = false,
organizationPremiumStatusMap = emptyMap(),
)
assertEquals(
@ -843,7 +857,7 @@ class VaultDataExtensionsTest {
folderItems = listOf(),
noFolderItems = listOf(),
trashItemsCount = 0,
totpItemsCount = 1,
totpItemsCount = 0,
// Verify item types count excludes CipherType.SSH_KEY when showSshKeys is false.
itemTypesCount = 4,
),
@ -855,7 +869,7 @@ class VaultDataExtensionsTest {
fun `toViewState should include SSH key vault items and type count if showSshKeys is true`() {
val vaultData = VaultData(
cipherViewList = listOf(
createMockCipherView(number = 1),
createMockCipherView(number = 1, organizationId = null),
createMockCipherView(number = 2, cipherType = CipherType.SSH_KEY),
),
collectionViewList = listOf(),
@ -871,6 +885,7 @@ class VaultDataExtensionsTest {
vaultFilterType = VaultFilterType.AllVaults,
hasMasterPassword = true,
showSshKeys = true,
organizationPremiumStatusMap = emptyMap(),
)
assertEquals(
@ -929,6 +944,7 @@ class VaultDataExtensionsTest {
vaultFilterType = VaultFilterType.AllVaults,
hasMasterPassword = true,
showSshKeys = true,
organizationPremiumStatusMap = emptyMap(),
)
assertEquals(

View file

@ -57,6 +57,7 @@ class VerificationCodeViewModelTest : BaseViewModelTest() {
private val mockUserAccount: UserState.Account = mockk {
every { isPremium } returns true
every { organizations } returns emptyList()
}
private val mockUserState: UserState = mockk {

View file

@ -16,4 +16,5 @@ fun createVerificationCodeItem(number: Int = 1) =
username = "mockUsername-$number",
hasPasswordReprompt = false,
orgUsesTotp = false,
orgId = null,
)