mirror of
https://github.com/bitwarden/android.git
synced 2026-08-01 18:53:39 -05:00
PM-37287: Feat: Add premium prompt for TOTP codes (#7090)
This commit is contained in:
+16
-1
@@ -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 = { },
|
||||
|
||||
+1
@@ -541,6 +541,7 @@ private val PREVIEW_COMMON_HANDLERS: VaultCommonItemTypeHandlers =
|
||||
onAttachmentPreviewClick = {},
|
||||
onCopyNotesClick = {},
|
||||
onPasswordHistoryClick = {},
|
||||
onPremiumRequiredClick = {},
|
||||
onUpgradeToPremiumClick = {},
|
||||
)
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
+26
-3
@@ -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.
|
||||
*/
|
||||
|
||||
+4
@@ -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)
|
||||
},
|
||||
|
||||
+38
-5
@@ -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 ->
|
||||
|
||||
+20
-2
@@ -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,
|
||||
)
|
||||
|
||||
@@ -1210,6 +1210,7 @@ Do you want to switch to this account?</string>
|
||||
<string name="item_moved_to_archived">Item moved to archive</string>
|
||||
<string name="item_moved_to_vault">Item moved to vault</string>
|
||||
<string name="archiving_items_is_a_premium_feature">Archiving items is a Premium feature. Your current plan does not include access to this feature.</string>
|
||||
<string name="totp_is_a_premium_feature">Authenticator key (TOTP) is a Premium feature. Your current plan does not include access to this feature.</string>
|
||||
<string name="upgrade_to_premium">Upgrade to Premium</string>
|
||||
<string name="plan">Plan</string>
|
||||
<string name="to_manage_your_premium_subscription_youll_need_to_login_to_your_web_vault_on_a_computer">To manage your Premium subscription, you’ll need to login to your web vault on a computer.</string>
|
||||
|
||||
Reference in New Issue
Block a user