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:
commit
fc08907b8a
51 changed files with 632 additions and 215 deletions
.github/workflows
Gemfile.lockapp/src
main
java/com/x8bit/bitwarden
data
auth/repository
autofill/manager
vault/manager
ui
auth/feature
completeregistration
masterpasswordguidance
platform
components/card
feature/search
vault/feature
res/values
test/java/com/x8bit/bitwarden
data
ui
auth/feature
masterpasswordguidance
removepassword
platform/feature
rootnav
search
vault/feature
addedit
item
itemlisting
movetoorganization
vault
verificationcode
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
|
@ -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:
|
||||
|
|
10
Gemfile.lock
10
Gemfile.lock
|
@ -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)
|
||||
|
|
|
@ -18,4 +18,5 @@ data class Organization(
|
|||
val shouldManageResetPassword: Boolean,
|
||||
val shouldUseKeyConnector: Boolean,
|
||||
val role: OrganizationType,
|
||||
val shouldUsersGetPremium: Boolean,
|
||||
)
|
||||
|
|
|
@ -22,6 +22,7 @@ fun SyncResponseJson.Profile.Organization.toOrganization(): Organization =
|
|||
shouldUseKeyConnector = this.shouldUseKeyConnector,
|
||||
role = this.type,
|
||||
shouldManageResetPassword = this.permissions.shouldManageResetPassword,
|
||||
shouldUsersGetPremium = this.shouldUsersGetPremium,
|
||||
)
|
||||
|
||||
/**
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -122,6 +122,7 @@ class TotpCodeManagerImpl(
|
|||
CipherRepromptType.NONE -> false
|
||||
},
|
||||
orgUsesTotp = cipher.organizationUseTotp,
|
||||
orgId = cipher.organizationId,
|
||||
)
|
||||
}
|
||||
.onFailure {
|
||||
|
|
|
@ -29,4 +29,5 @@ data class VerificationCodeItem(
|
|||
val username: String?,
|
||||
val hasPasswordReprompt: Boolean,
|
||||
val orgUsesTotp: Boolean,
|
||||
val orgId: String?,
|
||||
)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
||||
/**
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -404,6 +404,7 @@ private fun VaultItemContent(
|
|||
VaultItemSshKeyContent(
|
||||
commonState = viewState.common,
|
||||
sshKeyItemState = viewState.type,
|
||||
vaultCommonItemTypeHandlers = vaultCommonItemTypeHandlers,
|
||||
vaultSshKeyItemTypeHandlers = vaultSshKeyItemTypeHandlers,
|
||||
modifier = modifier,
|
||||
)
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 }
|
||||
}
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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. \nHere’s 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">You’ve been logged out because your device’s biometrics don’t 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>
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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) }
|
||||
|
|
|
@ -161,6 +161,7 @@ private val DEFAULT_ACCOUNT = UserState.Account(
|
|||
shouldManageResetPassword = false,
|
||||
shouldUseKeyConnector = true,
|
||||
role = OrganizationType.USER,
|
||||
shouldUsersGetPremium = false,
|
||||
),
|
||||
),
|
||||
needsMasterPassword = false,
|
||||
|
|
|
@ -386,6 +386,7 @@ class RootNavViewModelTest : BaseViewModelTest() {
|
|||
shouldManageResetPassword = false,
|
||||
shouldUseKeyConnector = true,
|
||||
role = OrganizationType.USER,
|
||||
shouldUsersGetPremium = false,
|
||||
),
|
||||
),
|
||||
needsMasterPassword = false,
|
||||
|
|
|
@ -938,6 +938,7 @@ private val DEFAULT_STATE: SearchState = SearchState(
|
|||
totpData = null,
|
||||
autofillSelectionData = null,
|
||||
isPremium = true,
|
||||
organizationPremiumStatusMap = emptyMap(),
|
||||
)
|
||||
|
||||
private fun createStateForAutofill(
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -4396,6 +4396,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
|||
shouldManageResetPassword = false,
|
||||
shouldUseKeyConnector = false,
|
||||
role = OrganizationType.ADMIN,
|
||||
shouldUsersGetPremium = false,
|
||||
),
|
||||
),
|
||||
isBiometricsEnabled = true,
|
||||
|
|
|
@ -565,6 +565,7 @@ class CipherViewExtensionsTest {
|
|||
shouldManageResetPassword = false,
|
||||
shouldUseKeyConnector = false,
|
||||
role = OrganizationType.ADMIN,
|
||||
shouldUsersGetPremium = false,
|
||||
),
|
||||
),
|
||||
isBiometricsEnabled = true,
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -4079,6 +4079,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
|||
fido2CreateCredentialRequest = null,
|
||||
isPremium = true,
|
||||
isRefreshing = false,
|
||||
organizationPremiumStatusMap = emptyMap(),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -16,4 +16,5 @@ fun createVerificationCodeItem(number: Int = 1) =
|
|||
username = "mockUsername-$number",
|
||||
hasPasswordReprompt = false,
|
||||
orgUsesTotp = false,
|
||||
orgId = null,
|
||||
)
|
||||
|
|
Loading…
Add table
Reference in a new issue