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.