diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemLoginContent.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemLoginContent.kt
index dbe8ba0b773..5323be77054 100644
--- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemLoginContent.kt
+++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemLoginContent.kt
@@ -147,6 +147,7 @@ fun VaultItemLoginContent(
onCopyTotpClick = vaultLoginItemTypeHandlers.onCopyTotpCodeClick,
onAuthenticatorHelpToolTipClick = vaultLoginItemTypeHandlers
.onAuthenticatorHelpToolTipClick,
+ onPremiumRequiredClick = vaultCommonItemTypeHandlers.onPremiumRequiredClick,
modifier = Modifier
.standardHorizontalMargin()
.fillMaxWidth()
@@ -285,12 +286,14 @@ private fun PasswordField(
}
}
+@Suppress("LongMethod")
@Composable
private fun TotpField(
totpCodeItemData: TotpCodeItemData,
enabled: Boolean,
onCopyTotpClick: () -> Unit,
onAuthenticatorHelpToolTipClick: () -> Unit,
+ onPremiumRequiredClick: () -> Unit,
modifier: Modifier = Modifier,
) {
if (enabled) {
@@ -333,7 +336,19 @@ private fun TotpField(
contentDescription = stringResource(id = BitwardenString.authenticator_key_help),
isExternalLink = true,
),
- supportingText = stringResource(id = BitwardenString.premium_subscription_required),
+ supportingContentPadding = PaddingValues(),
+ supportingContent = {
+ BitwardenClickableText(
+ label = stringResource(id = BitwardenString.premium_subscription_required),
+ onClick = onPremiumRequiredClick,
+ style = BitwardenTheme.typography.labelMedium,
+ innerPadding = PaddingValues(all = 16.dp),
+ cornerSize = 0.dp,
+ modifier = Modifier
+ .fillMaxWidth()
+ .testTag(tag = "TotpPremiumRequiredButton"),
+ )
+ },
enabled = false,
singleLine = false,
onValueChange = { },
diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemPassportContent.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemPassportContent.kt
index 72b8cecc9f2..a5b88ccd2a8 100644
--- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemPassportContent.kt
+++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemPassportContent.kt
@@ -541,6 +541,7 @@ private val PREVIEW_COMMON_HANDLERS: VaultCommonItemTypeHandlers =
onAttachmentPreviewClick = {},
onCopyNotesClick = {},
onPasswordHistoryClick = {},
+ onPremiumRequiredClick = {},
onUpgradeToPremiumClick = {},
)
diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemScreen.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemScreen.kt
index 82c1b4457f0..120742e51be 100644
--- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemScreen.kt
+++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemScreen.kt
@@ -311,10 +311,10 @@ private fun VaultItemDialogs(
onUpgradeToPremiumClick: () -> Unit,
) {
when (dialog) {
- is VaultItemState.DialogState.ArchiveRequiresPremium -> {
+ is VaultItemState.DialogState.RequiresPremium -> {
BitwardenTwoButtonDialog(
title = stringResource(id = BitwardenString.premium_subscription_required),
- message = stringResource(id = BitwardenString.archiving_items_is_a_premium_feature),
+ message = dialog.message(),
confirmButtonText = stringResource(id = BitwardenString.upgrade_to_premium),
dismissButtonText = stringResource(id = BitwardenString.cancel),
onConfirmClick = onUpgradeToPremiumClick,
diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModel.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModel.kt
index 5f6d59af3af..260772263a4 100644
--- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModel.kt
+++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModel.kt
@@ -303,6 +303,7 @@ class VaultItemViewModel @Inject constructor(
is VaultItemAction.Common.PasswordHistoryClick -> handlePasswordHistoryClick()
VaultItemAction.Common.ArchiveClick -> handleArchiveClick()
VaultItemAction.Common.UnarchiveClick -> handleUnarchiveClick()
+ VaultItemAction.Common.PremiumRequiredClick -> handlePremiumRequiredClick()
VaultItemAction.Common.UpgradeToPremiumClick -> handleUpgradeToPremiumClick()
}
}
@@ -690,7 +691,11 @@ class VaultItemViewModel @Inject constructor(
private fun handleArchiveClick() {
if (!state.hasPremium) {
mutableStateFlow.update {
- it.copy(dialog = VaultItemState.DialogState.ArchiveRequiresPremium)
+ it.copy(
+ dialog = VaultItemState.DialogState.RequiresPremium(
+ message = BitwardenString.archiving_items_is_a_premium_feature.asText(),
+ ),
+ )
}
return
}
@@ -741,6 +746,16 @@ class VaultItemViewModel @Inject constructor(
}
}
+ private fun handlePremiumRequiredClick() {
+ mutableStateFlow.update {
+ it.copy(
+ dialog = VaultItemState.DialogState.RequiresPremium(
+ message = BitwardenString.totp_is_a_premium_feature.asText(),
+ ),
+ )
+ }
+ }
+
private fun handleUpgradeToPremiumClick() {
updateDialogState(dialog = null)
if (premiumStateManager.isInAppUpgradeAvailable()) {
@@ -2335,10 +2350,13 @@ data class VaultItemState(
sealed class DialogState : Parcelable {
/**
- * Displays a dialog to the user indicating that archiving requires a Premium account.
+ * Displays a dialog to the user indicating that the feature they are interacting with
+ * requires a Premium account.
*/
@Parcelize
- data object ArchiveRequiresPremium : DialogState()
+ data class RequiresPremium(
+ val message: Text,
+ ) : DialogState()
/**
* Displays a generic dialog to the user.
@@ -2500,6 +2518,11 @@ sealed class VaultItemAction {
*/
data object UnarchiveClick : Common()
+ /**
+ * The user has clicked the Premium subscription required button.
+ */
+ data object PremiumRequiredClick : Common()
+
/**
* The user has clicked the upgrade to Premium button.
*/
diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/handlers/VaultCommonItemTypeHandlers.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/handlers/VaultCommonItemTypeHandlers.kt
index 3ebfebb3fdc..3c6bb079e5e 100644
--- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/handlers/VaultCommonItemTypeHandlers.kt
+++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/handlers/VaultCommonItemTypeHandlers.kt
@@ -20,6 +20,7 @@ data class VaultCommonItemTypeHandlers(
val onAttachmentPreviewClick: (VaultItemState.ViewState.Content.Common.AttachmentItem) -> Unit,
val onCopyNotesClick: () -> Unit,
val onPasswordHistoryClick: () -> Unit,
+ val onPremiumRequiredClick: () -> Unit,
val onUpgradeToPremiumClick: () -> Unit,
) {
@Suppress("UndocumentedPublicClass")
@@ -61,6 +62,9 @@ data class VaultCommonItemTypeHandlers(
onPasswordHistoryClick = {
viewModel.trySendAction(VaultItemAction.Common.PasswordHistoryClick)
},
+ onPremiumRequiredClick = {
+ viewModel.trySendAction(VaultItemAction.Common.PremiumRequiredClick)
+ },
onUpgradeToPremiumClick = {
viewModel.trySendAction(VaultItemAction.Common.UpgradeToPremiumClick)
},
diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemScreenTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemScreenTest.kt
index 1a0397ac0e7..74596982c36 100644
--- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemScreenTest.kt
+++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemScreenTest.kt
@@ -268,24 +268,35 @@ class VaultItemScreenTest : BitwardenComposeTest() {
}
@Test
- fun `ArchiveRequiresPremium dialog should display based on state`() {
+ fun `RequiresPremium dialog should display based on state`() {
composeTestRule.assertNoDialogExists()
mutableStateFlow.update {
- it.copy(dialog = VaultItemState.DialogState.ArchiveRequiresPremium)
+ it.copy(
+ dialog = VaultItemState.DialogState.RequiresPremium(
+ message = "You need Premium".asText(),
+ ),
+ )
}
composeTestRule
.onNodeWithText(text = "Premium subscription required")
.assert(hasAnyAncestor(isDialog()))
.assertIsDisplayed()
+ composeTestRule
+ .onNodeWithText(text = "You need Premium")
+ .assert(hasAnyAncestor(isDialog()))
+ .assertIsDisplayed()
}
- @Suppress("MaxLineLength")
@Test
- fun `ArchiveRequiresPremium dialog on upgrade to Premium click should emit UpgradeToPremiumClick`() {
+ fun `RequiresPremium dialog on upgrade to Premium click should emit UpgradeToPremiumClick`() {
composeTestRule.assertNoDialogExists()
mutableStateFlow.update {
- it.copy(dialog = VaultItemState.DialogState.ArchiveRequiresPremium)
+ it.copy(
+ dialog = VaultItemState.DialogState.RequiresPremium(
+ message = "You need Premium".asText(),
+ ),
+ )
}
composeTestRule
@@ -2374,6 +2385,28 @@ class VaultItemScreenTest : BitwardenComposeTest() {
}
}
+ @Test
+ fun `in login state, on premium required click should send PremiumRequiredClick`() {
+ mutableStateFlow.update { currentState ->
+ currentState.copy(
+ viewState = DEFAULT_LOGIN_VIEW_STATE.copy(
+ type = DEFAULT_LOGIN.copy(
+ canViewTotpCode = false,
+ ),
+ ),
+ )
+ }
+
+ composeTestRule.onNodeWithTextAfterScroll(text = "Authenticator key")
+ composeTestRule
+ .onNodeWithText(text = "Premium subscription required")
+ .performClick()
+
+ verify {
+ viewModel.trySendAction(VaultItemAction.Common.PremiumRequiredClick)
+ }
+ }
+
@Test
fun `in login state, on totp help tooltip click should send AuthenticatorHelpToolTipClick`() {
mutableStateFlow.update { currentState ->
diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModelTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModelTest.kt
index 32857a33446..d6e0d49ad68 100644
--- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModelTest.kt
+++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModelTest.kt
@@ -266,7 +266,7 @@ class VaultItemViewModelTest : BaseViewModelTest() {
}
@Test
- fun `ArchiveClick without Premium should show ArchiveRequiresPremium dialog`() = runTest {
+ fun `ArchiveClick without Premium should show RequiresPremium dialog`() = runTest {
mutableUserStateFlow.update {
it?.copy(accounts = listOf(DEFAULT_USER_ACCOUNT.copy(isPremium = false)))
}
@@ -277,7 +277,25 @@ class VaultItemViewModelTest : BaseViewModelTest() {
assertEquals(
DEFAULT_STATE.copy(
hasPremium = false,
- dialog = VaultItemState.DialogState.ArchiveRequiresPremium,
+ dialog = VaultItemState.DialogState.RequiresPremium(
+ message = BitwardenString.archiving_items_is_a_premium_feature.asText(),
+ ),
+ ),
+ viewModel.stateFlow.value,
+ )
+ }
+
+ @Test
+ fun `PremiumRequiredClick should show RequiresPremium dialog`() = runTest {
+ val viewModel = createViewModel(state = null)
+
+ viewModel.trySendAction(VaultItemAction.Common.PremiumRequiredClick)
+
+ assertEquals(
+ DEFAULT_STATE.copy(
+ dialog = VaultItemState.DialogState.RequiresPremium(
+ message = BitwardenString.totp_is_a_premium_feature.asText(),
+ ),
),
viewModel.stateFlow.value,
)
diff --git a/ui/src/main/res/values/strings.xml b/ui/src/main/res/values/strings.xml
index f620c2f4f0f..d6fe64ec833 100644
--- a/ui/src/main/res/values/strings.xml
+++ b/ui/src/main/res/values/strings.xml
@@ -1210,6 +1210,7 @@ Do you want to switch to this account?
Item moved to archive
Item moved to vault
Archiving items is a Premium feature. Your current plan does not include access to this feature.
+ Authenticator key (TOTP) is a Premium feature. Your current plan does not include access to this feature.
Upgrade to Premium
Plan
To manage your Premium subscription, you’ll need to login to your web vault on a computer.