Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ fun LazyListScope.vaultAddEditLoginItems(
TotpRow(
totpKey = loginState.totp,
canViewTotp = loginState.canViewPassword,
isAuthenticatorKeyPremiumGated = loginState.isAuthenticatorKeyPremiumGated,
loginItemTypeHandlers = loginItemTypeHandlers,
onTotpSetupClick = onTotpSetupClick,
modifier = Modifier.fillMaxWidth(),
Expand Down Expand Up @@ -362,6 +363,7 @@ private fun CoachMarkScope<AddEditItemCoachMark>.PasswordRow(
private fun TotpRow(
totpKey: String?,
canViewTotp: Boolean,
isAuthenticatorKeyPremiumGated: Boolean,
loginItemTypeHandlers: VaultAddEditLoginTypeHandlers,
onTotpSetupClick: () -> Unit,
modifier: Modifier = Modifier,
Expand Down Expand Up @@ -393,18 +395,31 @@ private fun TotpRow(
),
supportingContentPadding = PaddingValues(),
supportingContent = {
BitwardenClickableText(
label = stringResource(id = BitwardenString.set_up_authenticator_key),
onClick = onTotpSetupClick,
leadingIcon = painterResource(id = BitwardenDrawable.ic_camera_small),
style = BitwardenTheme.typography.labelMedium,
innerPadding = PaddingValues(all = 16.dp),
isEnabled = canViewTotp,
cornerSize = 0.dp,
modifier = Modifier
.fillMaxWidth()
.testTag("SetupTotpButton"),
)
if (isAuthenticatorKeyPremiumGated) {
BitwardenClickableText(
label = stringResource(id = BitwardenString.premium_subscription_required),
onClick = loginItemTypeHandlers.onTotpRequiresPremiumClick,
style = BitwardenTheme.typography.labelMedium,
innerPadding = PaddingValues(all = 16.dp),
cornerSize = 0.dp,
modifier = Modifier
.fillMaxWidth()
.testTag("LoginTotpPremiumRequired"),
)
} else {
BitwardenClickableText(
label = stringResource(id = BitwardenString.set_up_authenticator_key),
onClick = onTotpSetupClick,
leadingIcon = painterResource(id = BitwardenDrawable.ic_camera_small),
style = BitwardenTheme.typography.labelMedium,
innerPadding = PaddingValues(all = 16.dp),
isEnabled = canViewTotp,
cornerSize = 0.dp,
modifier = Modifier
.fillMaxWidth()
.testTag("SetupTotpButton"),
)
}
},
passwordFieldTestTag = "LoginTotpEntry",
cardStyle = CardStyle.Full,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,9 @@ fun VaultAddEditScreen(
onUpgradeToPremiumClick = {
viewModel.trySendAction(VaultAddEditAction.Common.UpgradeToPremiumClick)
},
onNavigateToPlanClick = {
viewModel.trySendAction(VaultAddEditAction.Common.NavigateToPlanClick)
},
onCameraPermissionSettingsClick = {
viewModel.trySendAction(
VaultAddEditAction.Common.CameraPermissionSettingsClick,
Expand Down Expand Up @@ -495,6 +498,7 @@ private fun VaultAddEditItemDialogs(
onRetryPinSetUpFido2Verification: () -> Unit,
onDismissFido2Verification: () -> Unit,
onUpgradeToPremiumClick: () -> Unit,
onNavigateToPlanClick: () -> Unit,
onCameraPermissionSettingsClick: () -> Unit,
) {
when (dialogState) {
Expand All @@ -504,7 +508,21 @@ private fun VaultAddEditItemDialogs(
message = stringResource(id = BitwardenString.archiving_items_is_a_premium_feature),
confirmButtonText = stringResource(id = BitwardenString.upgrade_to_premium),
dismissButtonText = stringResource(id = BitwardenString.cancel),
onConfirmClick = onUpgradeToPremiumClick,
onConfirmClick = onNavigateToPlanClick,
onDismissClick = onDismissRequest,
onDismissRequest = onDismissRequest,
)
}

is VaultAddEditState.DialogState.TotpRequiresPremium -> {
BitwardenTwoButtonDialog(
title = stringResource(id = BitwardenString.premium_subscription_required),
message = stringResource(
id = BitwardenString.authenticator_key_is_a_premium_feature,
),
confirmButtonText = stringResource(id = BitwardenString.upgrade_to_premium),
dismissButtonText = stringResource(id = BitwardenString.cancel),
onConfirmClick = onNavigateToPlanClick,
onDismissClick = onDismissRequest,
onDismissRequest = onDismissRequest,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ import com.x8bit.bitwarden.ui.vault.feature.addedit.util.appendFolderAndOwnerDat
import com.x8bit.bitwarden.ui.vault.feature.addedit.util.toAvailableFolders
import com.x8bit.bitwarden.ui.vault.feature.addedit.util.toDefaultAddTypeContent
import com.x8bit.bitwarden.ui.vault.feature.addedit.util.toItemType
import com.x8bit.bitwarden.ui.vault.feature.addedit.util.withAuthenticatorKeyPremiumGate
import com.x8bit.bitwarden.ui.vault.feature.addedit.util.toViewState
import com.x8bit.bitwarden.ui.vault.feature.addedit.util.validateCipherOrReturnErrorState
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.util.messageResourceId
Expand Down Expand Up @@ -187,14 +188,17 @@ class VaultAddEditViewModel @Inject constructor(
null
}

val hasPremium =
authRepository.userStateFlow.value?.activeAccount?.isPremium == true

VaultAddEditState(
isCardScannerEnabled = featureFlagManager
.getFeatureFlag(FlagKey.CardScanner) && !buildInfoManager.isFdroid,
vaultAddEditType = vaultAddEditType,
cipherType = vaultCipherType,
viewState = when (vaultAddEditType) {
is VaultAddEditType.AddItem -> {
autofillSelectionData
(autofillSelectionData
?.toDefaultAddTypeContent(isIndividualVaultDisabled)
?: autofillSaveItem?.toDefaultAddTypeContent(isIndividualVaultDisabled)
?: providerCreateCredentialRequest?.toDefaultAddTypeContent(
Expand All @@ -209,7 +213,8 @@ class VaultAddEditViewModel @Inject constructor(
),
isIndividualVaultDisabled = isIndividualVaultDisabled,
type = vaultCipherType.toItemType(),
)
))
.withAuthenticatorKeyPremiumGate(isPremium = hasPremium)
}

is VaultAddEditType.EditItem -> VaultAddEditState.ViewState.Loading
Expand All @@ -226,7 +231,7 @@ class VaultAddEditViewModel @Inject constructor(
shouldShowCoachMarkTour = false,
shouldClearSpecialCircumstance = autofillSelectionData == null,
defaultUriMatchType = settingsRepository.defaultUriMatchType,
hasPremium = authRepository.userStateFlow.value?.activeAccount?.isPremium == true,
hasPremium = hasPremium,
)
},
) {
Expand Down Expand Up @@ -352,6 +357,7 @@ class VaultAddEditViewModel @Inject constructor(
is VaultAddEditAction.Common.ArchiveClick -> handleArchiveClick()
is VaultAddEditAction.Common.UnarchiveClick -> handleUnarchiveClick()
VaultAddEditAction.Common.UpgradeToPremiumClick -> handleUpgradeToPremiumClick()
VaultAddEditAction.Common.NavigateToPlanClick -> handleNavigateToPlanClick()
is VaultAddEditAction.Common.ConfirmDeleteClick -> handleConfirmDeleteClick()
is VaultAddEditAction.Common.CloseClick -> handleCloseClick()
is VaultAddEditAction.Common.DismissDialog -> handleDismissDialog()
Expand Down Expand Up @@ -687,6 +693,11 @@ class VaultAddEditViewModel @Inject constructor(
}
}

private fun handleNavigateToPlanClick() {
mutableStateFlow.update { it.copy(dialog = null) }
sendEvent(VaultAddEditEvent.NavigateToPlanModal)
}

private fun handleConfirmDeleteClick() {
mutableStateFlow.update {
it.copy(
Expand Down Expand Up @@ -1213,6 +1224,16 @@ class VaultAddEditViewModel @Inject constructor(
VaultAddEditAction.ItemType.LoginType.LearnMoreClick -> {
handleLearnMoreClick()
}

VaultAddEditAction.ItemType.LoginType.TotpRequiresPremiumClick -> {
handleTotpRequiresPremiumClick()
}
}
}

private fun handleTotpRequiresPremiumClick() {
mutableStateFlow.update {
it.copy(dialog = VaultAddEditState.DialogState.TotpRequiresPremium)
}
}

Expand Down Expand Up @@ -2998,6 +3019,10 @@ data class VaultAddEditState(
* @property canViewPassword Indicates whether the current user can view and copy
* passwords associated with the login item.
* @property canEditItem Indicates whether the current user can edit the login item.
* @property isAuthenticatorKeyPremiumGated `true` when the active user lacks the
* entitlement required to use the authenticator key (TOTP) for this cipher β€”
* neither Premium nor an `organizationUseTotp` grant. Used solely to swap the
* authenticator key supporting content for a Premium upsell.
* @property fido2CredentialCreationDateTime Date and time the FIDO 2 credential was
* created.
*/
Expand All @@ -3008,6 +3033,7 @@ data class VaultAddEditState(
val totp: String? = null,
val canViewPassword: Boolean = true,
val canEditItem: Boolean = true,
val isAuthenticatorKeyPremiumGated: Boolean = false,
val uriList: List<UriItem> = listOf(
UriItem(
id = UUID.randomUUID().toString(),
Expand Down Expand Up @@ -3369,6 +3395,12 @@ data class VaultAddEditState(
*/
data object ArchiveRequiresPremium : DialogState()

/**
* Displays a dialog to the user indicating that the authenticator key (TOTP) requires a
* Premium account.
*/
data object TotpRequiresPremium : DialogState()

/**
* Displays a generic dialog to the user.
*/
Expand Down Expand Up @@ -3658,6 +3690,11 @@ sealed class VaultAddEditAction {
*/
data object UpgradeToPremiumClick : Common()

/**
* The user has clicked an upgrade CTA that should always navigate to the Plan screen.
*/
data object NavigateToPlanClick : Common()

/**
* The user has confirmed to deleted the cipher.
*/
Expand Down Expand Up @@ -3952,6 +3989,12 @@ sealed class VaultAddEditAction {
* User has clicked the call to action on the learn more help link.
*/
data object LearnMoreClick : LoginType()

/**
* The user has clicked the Premium subscription required CTA shown in place of the
* authenticator key when the active account lacks the Premium entitlement.
*/
data object TotpRequiresPremiumClick : LoginType()
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ import com.x8bit.bitwarden.ui.vault.feature.addedit.model.UriItem
* is clicked.
* @property onAuthenticatorHelpToolTipClick Handles the action when the authenticator help tooltip
* is clicked.
* @property onTotpRequiresPremiumClick Handles the action when the Premium subscription required
* CTA is clicked in place of the authenticator key for non-Premium accounts.
*/
@Suppress("LongParameterList")
data class VaultAddEditLoginTypeHandlers(
Expand All @@ -47,6 +49,7 @@ data class VaultAddEditLoginTypeHandlers(
val onStartLoginCoachMarkTour: () -> Unit,
val onDismissLearnAboutLoginsCard: () -> Unit,
val onAuthenticatorHelpToolTipClick: () -> Unit,
val onTotpRequiresPremiumClick: () -> Unit,
val onLearnMoreClick: () -> Unit,
) {
@Suppress("UndocumentedPublicClass")
Expand Down Expand Up @@ -112,6 +115,11 @@ data class VaultAddEditLoginTypeHandlers(
VaultAddEditAction.ItemType.LoginType.AuthenticatorHelpToolTipClick,
)
},
onTotpRequiresPremiumClick = {
viewModel.trySendAction(
VaultAddEditAction.ItemType.LoginType.TotpRequiresPremiumClick,
)
},
onCopyTotpKeyClick = { totpKey ->
viewModel.trySendAction(
VaultAddEditAction.ItemType.LoginType.CopyTotpKeyClick(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ fun CipherView.toViewState(
totp = totpData?.uri ?: login?.totp,
canViewPassword = this.viewPassword,
canEditItem = this.edit,
isAuthenticatorKeyPremiumGated = !isPremium && !this.organizationUseTotp,
uriList = login?.uris.toUriItems(),
fido2CredentialCreationDateTime = login
?.fido2Credentials
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,19 @@ fun VaultItemCipherType.toItemType(): VaultAddEditState.ViewState.Content.ItemTy
VaultAddEditState.ViewState.Content.ItemType.Passport()
}
}

/**
* Returns a copy of the [VaultAddEditState.ViewState] with the authenticator key Premium gate
* applied to its Login content (if any). Used to seed the gate for Add mode, where the Login
* state is constructed by factories that have no premium context. Edit and Clone modes already
* set the gate via `CipherView.toViewState`, which additionally honors `organizationUseTotp`.
*/
fun VaultAddEditState.ViewState.withAuthenticatorKeyPremiumGate(
isPremium: Boolean,
): VaultAddEditState.ViewState {
val content = this as? VaultAddEditState.ViewState.Content ?: return this
val login = content.type as? VaultAddEditState.ViewState.Content.ItemType.Login ?: return this
return content.copy(
type = login.copy(isAuthenticatorKeyPremiumGated = !isPremium),
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,8 @@ fun VaultItemLoginContent(
onCopyTotpClick = vaultLoginItemTypeHandlers.onCopyTotpCodeClick,
onAuthenticatorHelpToolTipClick = vaultLoginItemTypeHandlers
.onAuthenticatorHelpToolTipClick,
onPremiumRequiredClick = vaultLoginItemTypeHandlers
.onTotpRequiresPremiumClick,
modifier = Modifier
.standardHorizontalMargin()
.fillMaxWidth()
Expand Down Expand Up @@ -285,12 +287,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) {
Expand Down Expand Up @@ -333,7 +337,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("LoginTotpPremiumRequired"),
)
},
enabled = false,
singleLine = false,
onValueChange = { },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,9 @@ fun VaultItemScreen(
onUpgradeToPremiumClick = {
viewModel.trySendAction(VaultItemAction.Common.UpgradeToPremiumClick)
},
onNavigateToPlanClick = {
viewModel.trySendAction(VaultItemAction.Common.NavigateToPlanClick)
},
)

val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
Expand Down Expand Up @@ -301,6 +304,7 @@ fun VaultItemScreen(
}
}

@Suppress("LongMethod")
@Composable
private fun VaultItemDialogs(
dialog: VaultItemState.DialogState?,
Expand All @@ -309,6 +313,7 @@ private fun VaultItemDialogs(
onConfirmCloneWithoutFido2Credential: () -> Unit,
onConfirmRestoreAction: () -> Unit,
onUpgradeToPremiumClick: () -> Unit,
onNavigateToPlanClick: () -> Unit,
) {
when (dialog) {
is VaultItemState.DialogState.ArchiveRequiresPremium -> {
Expand All @@ -317,7 +322,21 @@ private fun VaultItemDialogs(
message = stringResource(id = BitwardenString.archiving_items_is_a_premium_feature),
confirmButtonText = stringResource(id = BitwardenString.upgrade_to_premium),
dismissButtonText = stringResource(id = BitwardenString.cancel),
onConfirmClick = onUpgradeToPremiumClick,
onConfirmClick = onNavigateToPlanClick,
onDismissClick = onDismissRequest,
onDismissRequest = onDismissRequest,
)
}

is VaultItemState.DialogState.TotpRequiresPremium -> {
BitwardenTwoButtonDialog(
title = stringResource(id = BitwardenString.premium_subscription_required),
message = stringResource(
id = BitwardenString.authenticator_key_is_a_premium_feature,
),
confirmButtonText = stringResource(id = BitwardenString.upgrade_to_premium),
dismissButtonText = stringResource(id = BitwardenString.cancel),
onConfirmClick = onNavigateToPlanClick,
onDismissClick = onDismissRequest,
onDismissRequest = onDismissRequest,
)
Expand Down
Loading
Loading