PM-45133: chore: Remove Premium Upgrade feature flag (#7544)

This commit is contained in:
David Perez
2026-10-29 15:02:37 +00:00
committed by GitHub
parent b3bb43bd2c
commit 471f29bfb2
6 changed files with 45 additions and 126 deletions
@@ -5,7 +5,6 @@ import com.bitwarden.core.data.manager.model.FlagKey
import com.bitwarden.core.data.repository.model.DataState
import com.bitwarden.data.repository.model.Environment
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountJson
import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson
import com.x8bit.bitwarden.data.auth.repository.util.activeUserIdChangesFlow
import com.x8bit.bitwarden.data.billing.model.PremiumCard
@@ -52,7 +51,7 @@ class PremiumStateManagerImpl(
private val billingRepository: BillingRepository,
private val settingsDiskSource: SettingsDiskSource,
vaultRepository: VaultRepository,
private val featureFlagManager: FeatureFlagManager,
featureFlagManager: FeatureFlagManager,
environmentRepository: EnvironmentRepository,
pushManager: PushManager,
private val clock: Clock,
@@ -130,7 +129,7 @@ class PremiumStateManagerImpl(
combine(
authDiskSource.userStateFlow.map { it?.activeAccount },
billingRepository.isInAppBillingSupportedFlow,
featureFlagManager.getFeatureFlagFlow(FlagKey.MobilePremiumUpgrade),
upgradeLifecycleStateFlow,
authDiskSource.activeUserIdChangesFlow.flatMapLatest { userId ->
userId
?.let { id ->
@@ -144,47 +143,35 @@ class PremiumStateManagerImpl(
) {
account,
isInAppBillingSupported,
featureFlagEnabled,
lifecycle,
isUpgradeCardDismissed,
vaultDataState,
->
BannerInputs(
account = account,
isInAppBillingSupported = isInAppBillingSupported,
featureFlagEnabled = featureFlagEnabled,
isUpgradeCardDismissed = isUpgradeCardDismissed,
vaultDataState = vaultDataState,
)
}
.combine(upgradeLifecycleStateFlow) { inputs, lifecycle ->
val profile = inputs.account?.profile ?: return@combine PremiumCard.NONE
if (!inputs.featureFlagEnabled) return@combine PremiumCard.NONE
val initialCard = when (lifecycle) {
UpgradeLifecycleState.Free -> PremiumCard.UPGRADE
UpgradeLifecycleState.UpgradePending -> PremiumCard.NONE
is UpgradeLifecycleState.Premium -> {
lifecycle.subscriptionStatus.premiumCardState()
}
}
when (initialCard) {
PremiumCard.UPGRADE -> {
val isAccountOldEnough = profile.creationDate.isOlderThanDays(
days = PREMIUM_UPGRADE_MINIMUM_ACCOUNT_AGE_DAYS,
clock = clock,
)
val itemCount = inputs.vaultDataState.activeVaultItemCount()
val showCard = inputs.isInAppBillingSupported &&
isAccountOldEnough &&
itemCount >= PREMIUM_UPGRADE_MINIMUM_VAULT_ITEMS &&
!inputs.isUpgradeCardDismissed
initialCard.takeIf { showCard } ?: PremiumCard.NONE
}
PremiumCard.NEEDS_ATTENTION,
PremiumCard.NONE,
-> initialCard
}
val profile = account?.profile ?: return@combine PremiumCard.NONE
val initialCard = when (lifecycle) {
UpgradeLifecycleState.Free -> PremiumCard.UPGRADE
UpgradeLifecycleState.UpgradePending -> PremiumCard.NONE
is UpgradeLifecycleState.Premium -> lifecycle.subscriptionStatus.premiumCardState()
}
when (initialCard) {
PremiumCard.UPGRADE -> {
val isAccountOldEnough = profile.creationDate.isOlderThanDays(
days = PREMIUM_UPGRADE_MINIMUM_ACCOUNT_AGE_DAYS,
clock = clock,
)
val itemCount = vaultDataState.activeVaultItemCount()
val showCard = isInAppBillingSupported &&
isAccountOldEnough &&
itemCount >= PREMIUM_UPGRADE_MINIMUM_VAULT_ITEMS &&
!isUpgradeCardDismissed
initialCard.takeIf { showCard } ?: PremiumCard.NONE
}
PremiumCard.NEEDS_ATTENTION,
PremiumCard.NONE,
-> initialCard
}
}
.stateIn(
scope = unconfinedScope,
started = SharingStarted.Eagerly,
@@ -211,17 +198,16 @@ class PremiumStateManagerImpl(
* personal subscription to manage.
*/
override val isPlanRowEligibleFlow: StateFlow<Boolean> =
combine(
authDiskSource.userStateFlow,
featureFlagManager.getFeatureFlagFlow(FlagKey.MobilePremiumUpgrade),
) { userState, featureFlagEnabled ->
val profile = userState?.activeAccount?.profile ?: return@combine false
val hasPremium = profile.hasPremiumPersonally == true ||
profile.hasPremiumFromOrganization == true
val isPremiumFromSelf = profile.hasPremiumPersonally == true
val isOrgOnlyPremium = hasPremium && !isPremiumFromSelf
featureFlagEnabled && !isOrgOnlyPremium
}
authDiskSource
.userStateFlow
.map { userState ->
val profile = userState?.activeAccount?.profile ?: return@map false
val hasPremium = profile.hasPremiumPersonally == true ||
profile.hasPremiumFromOrganization == true
val isPremiumFromSelf = profile.hasPremiumPersonally == true
val isOrgOnlyPremium = hasPremium && !isPremiumFromSelf
!isOrgOnlyPremium
}
.stateIn(
scope = unconfinedScope,
started = SharingStarted.Eagerly,
@@ -311,8 +297,7 @@ class PremiumStateManagerImpl(
override val isSelfHosted: Boolean get() = isSelfHostedFlow.value
override fun isInAppUpgradeAvailable(): Boolean =
billingRepository.isInAppBillingSupportedFlow.value &&
featureFlagManager.getFeatureFlag(FlagKey.MobilePremiumUpgrade)
billingRepository.isInAppBillingSupportedFlow.value
override fun dismissPremiumUpgradeBanner() {
val activeUserId = authDiskSource.userState?.activeUserId ?: return
@@ -406,14 +391,6 @@ class PremiumStateManagerImpl(
}
}
private data class BannerInputs(
val account: AccountJson?,
val isInAppBillingSupported: Boolean,
val featureFlagEnabled: Boolean,
val isUpgradeCardDismissed: Boolean,
val vaultDataState: DataState<VaultData>,
)
/**
* Returns a [PremiumCard] for the given [SubscriptionStatusState] and subscription substate.
*/
@@ -73,15 +73,8 @@ class PremiumStateManagerTest {
every { vaultDataStateFlow } returns mutableVaultDataStateFlow
}
private val mutableMobilePremiumUpgradeFlagFlow = MutableStateFlow(true)
private val mutableDebugDisableSelfHostPremiumCheckFlagFlow = MutableStateFlow(false)
private val featureFlagManager: FeatureFlagManager = mockk {
every {
getFeatureFlagFlow(FlagKey.MobilePremiumUpgrade)
} returns mutableMobilePremiumUpgradeFlagFlow
every {
getFeatureFlag(FlagKey.MobilePremiumUpgrade)
} answers { mutableMobilePremiumUpgradeFlagFlow.value }
every {
getFeatureFlag(FlagKey.DebugDisableSelfHostPremiumCheck)
} answers { mutableDebugDisableSelfHostPremiumCheckFlagFlow.value }
@@ -141,15 +134,6 @@ class PremiumStateManagerTest {
}
}
@Test
fun `ineligible when feature flag is disabled should emit NONE`() = runTest {
mutableMobilePremiumUpgradeFlagFlow.value = false
val manager = createManager()
manager.premiumCardStateFlow.test {
assertEquals(PremiumCard.NONE, awaitItem())
}
}
@Test
fun `ineligible when banner is dismissed should emit NONE`() = runTest {
fakeSettingsDiskSource.storePremiumUpgradeBannerDismissed(
@@ -356,7 +340,7 @@ class PremiumStateManagerTest {
}
@Test
fun `isInAppUpgradeAvailable should return true when billing supported and flag enabled`() {
fun `isInAppUpgradeAvailable should return true when billing is supported`() {
val manager = createManager()
assertTrue(manager.isInAppUpgradeAvailable())
}
@@ -368,21 +352,6 @@ class PremiumStateManagerTest {
assertFalse(manager.isInAppUpgradeAvailable())
}
@Test
fun `isInAppUpgradeAvailable should return false when feature flag disabled`() {
mutableMobilePremiumUpgradeFlagFlow.value = false
val manager = createManager()
assertFalse(manager.isInAppUpgradeAvailable())
}
@Test
fun `isInAppUpgradeAvailable should return false when both conditions are false`() {
mutableIsInAppBillingSupportedFlow.value = false
mutableMobilePremiumUpgradeFlagFlow.value = false
val manager = createManager()
assertFalse(manager.isInAppUpgradeAvailable())
}
@Test
fun `isSelfHosted should return false on cloud environment regardless of flag`() {
fakeEnvironmentRepository.environment = Environment.Prod.Us
@@ -474,14 +443,13 @@ class PremiumStateManagerTest {
}
@Test
fun `isPlanRowEligibleFlow emits true for a free user when feature flag is enabled`() =
runTest {
fakeAuthDiskSource.userState = userStateJsonWith(account = createAccountJson())
val manager = createManager()
manager.isPlanRowEligibleFlow.test {
assertTrue(awaitItem())
}
fun `isPlanRowEligibleFlow emits true when user is a free user`() = runTest {
fakeAuthDiskSource.userState = userStateJsonWith(account = createAccountJson())
val manager = createManager()
manager.isPlanRowEligibleFlow.test {
assertTrue(awaitItem())
}
}
@Test
fun `isPlanRowEligibleFlow emits true for a personal Premium user`() = runTest {
@@ -506,15 +474,6 @@ class PremiumStateManagerTest {
}
}
@Test
fun `isPlanRowEligibleFlow emits false when feature flag is disabled`() = runTest {
mutableMobilePremiumUpgradeFlagFlow.value = false
val manager = createManager()
manager.isPlanRowEligibleFlow.test {
assertFalse(awaitItem())
}
}
@Test
fun `isPlanRowEligibleFlow emits false when userState is null`() = runTest {
fakeAuthDiskSource.userState = null
@@ -34,7 +34,6 @@ sealed class FlagKey<out T : Any> {
NoLogoutOnKdfChange,
MigrateMyVaultToMyItems,
CardScanner,
MobilePremiumUpgrade,
ManageDevices,
AttachmentUpdates,
V2EncryptionJitPassword,
@@ -95,14 +94,6 @@ sealed class FlagKey<out T : Any> {
override val defaultValue: Boolean = false
}
/**
* Data object holding the feature flag key for the mobile Premium upgrade feature.
*/
data object MobilePremiumUpgrade : FlagKey<Boolean>() {
override val keyName: String = "pm-31697-premium-upgrade-path"
override val defaultValue: Boolean = false
}
/**
* Data object holding the feature flag key for the Attachment Feature Updates.
*/
@@ -20,10 +20,6 @@ class FlagKeyTest {
FlagKey.MigrateMyVaultToMyItems.keyName,
"pm-20558-migrate-myvault-to-myitems",
)
assertEquals(
FlagKey.MobilePremiumUpgrade.keyName,
"pm-31697-premium-upgrade-path",
)
assertEquals(
FlagKey.AttachmentUpdates.keyName,
"pm-34224-mobile-attachment-updates",
@@ -93,7 +89,6 @@ class FlagKeyTest {
FlagKey.BitwardenAuthenticationEnabled,
FlagKey.ForceUpdateKdfSettings,
FlagKey.MigrateMyVaultToMyItems,
FlagKey.MobilePremiumUpgrade,
FlagKey.AttachmentUpdates,
FlagKey.V2EncryptionJitPassword,
FlagKey.V2EncryptionKeyConnector,
@@ -28,7 +28,6 @@ fun <T : Any> FlagKey<T>.ListItemContent(
FlagKey.NoLogoutOnKdfChange,
FlagKey.MigrateMyVaultToMyItems,
FlagKey.CardScanner,
FlagKey.MobilePremiumUpgrade,
FlagKey.ManageDevices,
FlagKey.AttachmentUpdates,
FlagKey.V2EncryptionJitPassword,
@@ -94,7 +93,6 @@ private fun <T : Any> FlagKey<T>.getDisplayLabel(): String = when (this) {
FlagKey.MigrateMyVaultToMyItems -> stringResource(BitwardenString.migrate_my_vault_to_my_items)
FlagKey.CardScanner -> stringResource(BitwardenString.scan_card)
FlagKey.MobilePremiumUpgrade -> stringResource(BitwardenString.mobile_premium_upgrade)
FlagKey.ManageDevices -> stringResource(BitwardenString.manage_devices)
FlagKey.AttachmentUpdates -> stringResource(BitwardenString.attachment_updates)
FlagKey.V2EncryptionJitPassword -> stringResource(BitwardenString.v2_encryption_jit_password)
@@ -48,7 +48,6 @@
<string name="migrate_my_vault_to_my_items">Migrate My Vault to My Items</string>
<string name="trigger_cookie_acquisition">Trigger cookie acquisition</string>
<string name="clear_sso_cookies">Clear SSO cookies</string>
<string name="mobile_premium_upgrade">Mobile Premium Upgrade</string>
<string name="attachment_updates">Attachment Updates</string>
<string name="v2_encryption_tde">V2 Encryption - TDE</string>
<string name="v2_encryption_key_connector">V2 Encryption - Key Connector</string>