[PM-37465] fix: Gate Plan row and Upgraded card on personal Premium (#6930)

This commit is contained in:
Patrick Honkonen authored and GitHub committed 2026-05-18 20:48:48 +00:00
1 parent bc93d7c311
commit 1f8280f76d
65 files changed
+654 -147

No files matched your search

@@ -42,7 +42,11 @@ data class AccountJson(
* @property name The user's name (if applicable).
* @property stamp The account's security stamp (if applicable).
* @property organizationId The ID of the associated organization (if applicable).
* @property hasPremium True if the user has a Premium account.
* @property hasPremiumPersonally True if the user has personal Premium (i.e., a personal
* subscription not derived from any organization membership).
* @property hasPremiumFromOrganization True if any organization the user is a member of grants
* Premium features. `null` when the value has not yet been synced (e.g., immediately after
* token-based login before the first sync completes).
* @property avatarColorHex Hex color value for a user's avatar in the "#AARRGGBB" format.
* @property forcePasswordResetReason Describes the reason for a forced password reset.
* @property kdfType The KDF type.
@@ -80,7 +84,10 @@ data class AccountJson(
val avatarColorHex: String?,
@SerialName("hasPremiumPersonally")
val hasPremium: Boolean?,
val hasPremiumPersonally: Boolean?,
@SerialName("hasPremiumFromOrganization")
val hasPremiumFromOrganization: Boolean?,
@SerialName("forcePasswordResetReason")
val forcePasswordResetReason: ForcePasswordResetReason?,
@@ -42,7 +42,12 @@ data class UserState(
* @property name The user's name (if applicable).
* @property avatarColorHex Hex color value for a user's avatar in the "#AARRGGBB" format.
* @property environment The [Environment] associated with the user's account.
* @property isPremium `true` if the account has a Premium membership.
* @property isPremium `true` if the account has a Premium membership from any source (personal
* subscription or organization-granted).
* @property isPremiumFromSelf `true` if the account has a personal Premium subscription. This
* is `false` for users whose only Premium access is granted by an organization they are a
* member of. Use this when behavior should be gated on the user's own subscription, not on
* organization-granted Premium.
* @property isLoggedIn `true` if the account is logged in, or `false` if it requires additional
* authentication to view their vault.
* @property isVaultUnlocked Whether the user's vault is currently unlocked.
@@ -66,6 +71,7 @@ data class UserState(
val avatarColorHex: String,
val environment: Environment,
val isPremium: Boolean,
val isPremiumFromSelf: Boolean,
val isLoggedIn: Boolean,
val isVaultUnlocked: Boolean,
val needsPasswordReset: Boolean,
@@ -113,6 +119,7 @@ data class UserState(
avatarColorHex = "".toHexColorRepresentation(),
environment = Environment.Us,
isPremium = false,
isPremiumFromSelf = false,
isLoggedIn = false,
isVaultUnlocked = false,
needsPasswordReset = false,
@@ -31,7 +31,8 @@ fun GetTokenResponseJson.Success.toUserState(
stamp = null,
organizationId = null,
avatarColorHex = null,
hasPremium = jwtTokenData.hasPremium,
hasPremiumPersonally = jwtTokenData.hasPremium,
hasPremiumFromOrganization = null,
forcePasswordResetReason = this.toForcePasswordResetReason(),
kdfType = this.kdfType,
kdfIterations = this.kdfIterations,
@@ -83,7 +83,8 @@ fun UserStateJson.toUpdatedUserStateJson(
.copy(
avatarColorHex = syncProfile.avatarColor,
stamp = syncProfile.securityStamp,
hasPremium = syncProfile.isPremium || syncProfile.isPremiumFromOrganization,
hasPremiumPersonally = syncProfile.isPremium,
hasPremiumFromOrganization = syncProfile.isPremiumFromOrganization,
isTwoFactorEnabled = syncProfile.isTwoFactorEnabled,
creationDate = syncProfile.creationDate,
userDecryptionOptions = userDecryptionOptions,
@@ -253,7 +254,9 @@ fun UserStateJson.toUserState(
.settings
.environmentUrlData
.toEnvironmentUrlsOrDefault(),
isPremium = profile.hasPremium == true,
isPremium = profile.hasPremiumPersonally == true ||
profile.hasPremiumFromOrganization == true,
isPremiumFromSelf = profile.hasPremiumPersonally == true,
isLoggedIn = userAccountTokens
.find { it.userId == userId }
?.isLoggedIn == true,
@@ -26,6 +26,12 @@ interface PremiumStateManager {
*/
val isUpgradedToPremiumCardEligibleFlow: StateFlow<Boolean>
/**
* Emits `true` when the active user is eligible to see the Plan row in Settings, or `false`
* otherwise.
*/
val isPlanRowEligibleFlow: StateFlow<Boolean>
/**
* Returns `true` when the in-app upgrade flow is available, or `false` otherwise.
*/
@@ -94,6 +94,32 @@ class PremiumStateManagerImpl(
initialValue = false,
)
/**
* Eligibility is keyed on the user holding personal Premium (or being eligible to purchase
* it). Organization-granted Premium does not surface the Plan row, since the user has no
* personal subscription to manage.
*/
override val isPlanRowEligibleFlow: StateFlow<Boolean> =
combine(
authRepository.userStateFlow,
featureFlagManager.getFeatureFlagFlow(FlagKey.MobilePremiumUpgrade),
) { userState, featureFlagEnabled ->
val activeAccount = userState?.activeAccount ?: return@combine false
val isOrgOnlyPremium = activeAccount.isPremium && !activeAccount.isPremiumFromSelf
featureFlagEnabled && !isOrgOnlyPremium
}
.stateIn(
scope = unconfinedScope,
started = SharingStarted.Eagerly,
initialValue = false,
)
/**
* The card surfaces only while the active user holds personal Premium. This guards against
* non-personal upgrade signals (e.g., the debug menu trigger or a stray
* `PREMIUM_STATUS_CHANGED` push for an organization grant) marking the card pending for users
* with no personal subscription to celebrate.
*/
@OptIn(ExperimentalCoroutinesApi::class)
override val isUpgradedToPremiumCardEligibleFlow: StateFlow<Boolean> =
authDiskSource
@@ -109,7 +135,12 @@ class PremiumStateManagerImpl(
settingsDiskSource
.getUpgradedToPremiumCardConsumedFlow(userId)
.map { it ?: false },
) { isPending, isConsumed -> isPending && !isConsumed }
authRepository
.userStateFlow
.map { it?.activeAccount?.isPremiumFromSelf == true },
) { isPending, isConsumed, isPremiumFromSelf ->
isPending && !isConsumed && isPremiumFromSelf
}
}
}
.distinctUntilChanged()
@@ -131,23 +162,22 @@ class PremiumStateManagerImpl(
}
.launchIn(unconfinedScope)
// Sync-delta detection: observe the active user's premium flag transitioning false → true
// (e.g., F-Droid users without push support). NOTE: UserState.Account.isPremium is
// derived from `hasPremium = isPremium || isPremiumFromOrganization` so this path may
// also fire for organization-granted premium. The push path (above) is personal-only and
// takes precedence on flavors that support it.
// Sync-delta detection: observe the active user's personal premium flag transitioning
// false → true (e.g., F-Droid users without push support). Keyed on `isPremiumFromSelf`
// so that organization-granted premium does not trigger the personal-upgrade card.
authRepository
.userStateFlow
.map { state ->
state?.activeAccount?.let { it.userId to it.isPremium }
state?.activeAccount?.let { it.userId to it.isPremiumFromSelf }
}
.distinctUntilChanged()
.scanPairs()
.onEach { (previous, current) ->
if (current == null) return@onEach
val (currentUserId, currentIsPremium) = current
if (!currentIsPremium) return@onEach
// Same user transitioning from non-premium to premium counts as an upgrade.
val (currentUserId, currentIsPremiumFromSelf) = current
if (!currentIsPremiumFromSelf) return@onEach
// Same user transitioning from non-personal-premium to personal-premium counts as
// an upgrade.
if (previous?.first == currentUserId && !previous.second) {
markUpgradedToPremiumCardPending(userId = currentUserId)
}
@@ -4,7 +4,6 @@ import androidx.annotation.DrawableRes
import androidx.compose.material3.Text
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import com.bitwarden.core.data.manager.model.FlagKey
import com.bitwarden.ui.platform.base.BaseViewModel
import com.bitwarden.ui.platform.base.DeferredBackgroundEvent
import com.bitwarden.ui.platform.resource.BitwardenDrawable
@@ -13,7 +12,6 @@ import com.bitwarden.ui.util.Text
import com.bitwarden.ui.util.asText
import com.x8bit.bitwarden.data.billing.manager.PremiumStateManager
import com.x8bit.bitwarden.data.billing.manager.UPGRADED_TO_PREMIUM_LEARN_MORE_URL
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManager
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager
import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance
@@ -34,7 +32,6 @@ import javax.inject.Inject
class SettingsViewModel @Inject constructor(
specialCircumstanceManager: SpecialCircumstanceManager,
firstTimeActionManager: FirstTimeActionManager,
featureFlagManager: FeatureFlagManager,
private val premiumStateManager: PremiumStateManager,
savedStateHandle: SavedStateHandle,
) : BaseViewModel<SettingsState, SettingsEvent, SettingsAction>(
@@ -43,8 +40,7 @@ class SettingsViewModel @Inject constructor(
securityCount = firstTimeActionManager.allSecuritySettingsBadgeCountFlow.value,
autoFillCount = firstTimeActionManager.allAutofillSettingsBadgeCountFlow.value,
vaultCount = firstTimeActionManager.allVaultSettingsBadgeCountFlow.value,
isMobilePremiumUpgradeEnabled = featureFlagManager
.getFeatureFlag(FlagKey.MobilePremiumUpgrade),
isPlanRowEligible = premiumStateManager.isPlanRowEligibleFlow.value,
isUpgradedToPremiumCardEligible = premiumStateManager
.isUpgradedToPremiumCardEligibleFlow
.value,
@@ -66,13 +62,9 @@ class SettingsViewModel @Inject constructor(
.onEach(::sendAction)
.launchIn(viewModelScope)
featureFlagManager
.getFeatureFlagFlow(FlagKey.MobilePremiumUpgrade)
.map {
SettingsAction.Internal.MobilePremiumUpgradeFlagUpdate(
isMobilePremiumUpgradeEnabled = it,
)
}
premiumStateManager
.isPlanRowEligibleFlow
.map { SettingsAction.Internal.PlanRowEligibilityReceive(isEligible = it) }
.onEach(::sendAction)
.launchIn(viewModelScope)
@@ -103,8 +95,8 @@ class SettingsViewModel @Inject constructor(
handleSettingsNotificationCountUpdate(action)
}
is SettingsAction.Internal.MobilePremiumUpgradeFlagUpdate -> {
handleMobilePremiumUpgradeFlagUpdate(action)
is SettingsAction.Internal.PlanRowEligibilityReceive -> {
handlePlanRowEligibilityReceive(action)
}
is SettingsAction.Internal.UpgradedToPremiumCardEligibilityReceive -> {
@@ -145,15 +137,10 @@ class SettingsViewModel @Inject constructor(
}
}
private fun handleMobilePremiumUpgradeFlagUpdate(
action: SettingsAction.Internal.MobilePremiumUpgradeFlagUpdate,
private fun handlePlanRowEligibilityReceive(
action: SettingsAction.Internal.PlanRowEligibilityReceive,
) {
mutableStateFlow.update {
it.copy(
isMobilePremiumUpgradeEnabled =
action.isMobilePremiumUpgradeEnabled,
)
}
mutableStateFlow.update { it.copy(isPlanRowEligible = action.isEligible) }
}
private fun handleSettingsClick(action: SettingsAction.SettingsClick) {
@@ -197,7 +184,7 @@ data class SettingsState(
private val autoFillCount: Int,
private val securityCount: Int,
private val vaultCount: Int,
private val isMobilePremiumUpgradeEnabled: Boolean = false,
private val isPlanRowEligible: Boolean,
private val isUpgradedToPremiumCardEligible: Boolean = false,
) {
val shouldShowCloseButton: Boolean = isPreAuth
@@ -209,12 +196,12 @@ data class SettingsState(
val shouldShowUpgradedToPremiumCard: Boolean = !isPreAuth && isUpgradedToPremiumCardEligible
/**
* Whether the plan row should be shown. The row is visible when the
* mobile premium upgrade feature flag is enabled and the user is
* authenticated.
* Whether the plan row should be shown. The row is visible post-authentication when the user
* is eligible per [PremiumStateManager.isPlanRowEligibleFlow] — currently, when the in-app
* upgrade feature is enabled and the user is not relying solely on organization-granted
* Premium.
*/
private val shouldShowPlanRow: Boolean =
!isPreAuth && isMobilePremiumUpgradeEnabled
private val shouldShowPlanRow: Boolean = !isPreAuth && isPlanRowEligible
val settingRows: ImmutableList<Settings> = Settings
.entries
@@ -335,10 +322,10 @@ sealed class SettingsAction {
) : Internal()
/**
* Update the mobile premium upgrade feature flag state.
* Indicates that the Plan row eligibility has been updated.
*/
data class MobilePremiumUpgradeFlagUpdate(
val isMobilePremiumUpgradeEnabled: Boolean,
data class PlanRowEligibilityReceive(
val isEligible: Boolean,
) : Internal()
/**
@@ -1416,6 +1416,7 @@ private val DEFAULT_ACCOUNT = UserState.Account(
environment = Environment.Us,
avatarColorHex = "#aa00aa",
isPremium = true,
isPremiumFromSelf = true,
isLoggedIn = true,
isVaultUnlocked = true,
needsPasswordReset = false,
@@ -1570,7 +1570,8 @@ private val USER_STATE = UserStateJson(
stamp = "stamp",
organizationId = "organizationId",
avatarColorHex = "avatarColorHex",
hasPremium = true,
hasPremiumPersonally = true,
hasPremiumFromOrganization = null,
forcePasswordResetReason = ForcePasswordResetReason.ADMIN_FORCE_PASSWORD_RESET,
kdfType = KdfTypeJson.ARGON2_ID,
kdfIterations = 600000,
@@ -1165,7 +1165,8 @@ private val ACCOUNT: AccountJson = AccountJson(
email = EMAIL,
isEmailVerified = true,
name = "Bitwarden Tester",
hasPremium = false,
hasPremiumPersonally = false,
hasPremiumFromOrganization = null,
stamp = null,
organizationId = null,
avatarColorHex = null,
@@ -177,7 +177,8 @@ private val ACCOUNT: AccountJson = AccountJson(
email = EMAIL,
isEmailVerified = true,
name = "Bitwarden Tester",
hasPremium = false,
hasPremiumPersonally = false,
hasPremiumFromOrganization = null,
stamp = null,
organizationId = null,
avatarColorHex = null,
@@ -344,7 +344,8 @@ private val PROFILE_1 = AccountJson.Profile(
email = EMAIL,
isEmailVerified = true,
name = "Bitwarden Tester",
hasPremium = false,
hasPremiumPersonally = false,
hasPremiumFromOrganization = null,
stamp = null,
organizationId = null,
avatarColorHex = null,
@@ -371,7 +372,8 @@ private val ACCOUNT_2 = AccountJson(
email = EMAIL_2,
isEmailVerified = true,
name = "Bitwarden Tester 2",
hasPremium = false,
hasPremiumPersonally = false,
hasPremiumFromOrganization = null,
stamp = null,
organizationId = null,
avatarColorHex = null,
@@ -289,7 +289,8 @@ private val DEFAULT_ACCOUNT = AccountJson(
email = "test@bitwarden.com",
isEmailVerified = true,
name = "Bitwarden Tester",
hasPremium = false,
hasPremiumPersonally = false,
hasPremiumFromOrganization = null,
stamp = null,
organizationId = null,
avatarColorHex = null,
@@ -313,7 +314,8 @@ private val UPDATED_ACCOUNT = AccountJson(
email = "test@bitwarden.com",
isEmailVerified = true,
name = "Bitwarden Tester",
hasPremium = false,
hasPremiumPersonally = false,
hasPremiumFromOrganization = null,
stamp = null,
organizationId = null,
avatarColorHex = null,
@@ -289,7 +289,8 @@ private val ACCOUNT_1 = AccountJson(
email = "test@bitwarden.com",
isEmailVerified = true,
name = "Bitwarden Tester",
hasPremium = false,
hasPremiumPersonally = false,
hasPremiumFromOrganization = null,
stamp = null,
organizationId = null,
avatarColorHex = null,
@@ -316,7 +317,8 @@ private val ACCOUNT_2 = AccountJson(
email = EMAIL_2,
isEmailVerified = true,
name = "Bitwarden Tester 2",
hasPremium = false,
hasPremiumPersonally = false,
hasPremiumFromOrganization = null,
stamp = null,
organizationId = null,
avatarColorHex = null,
@@ -327,7 +327,8 @@ private val PROFILE_1 = AccountJson.Profile(
email = EMAIL_1,
isEmailVerified = true,
name = "Bitwarden Tester",
hasPremium = false,
hasPremiumPersonally = false,
hasPremiumFromOrganization = null,
stamp = null,
organizationId = null,
avatarColorHex = null,
@@ -352,7 +353,8 @@ private val ACCOUNT_2 = AccountJson(
email = EMAIL_2,
isEmailVerified = true,
name = "Bitwarden Tester 2",
hasPremium = false,
hasPremiumPersonally = false,
hasPremiumFromOrganization = null,
stamp = null,
organizationId = null,
avatarColorHex = null,
@@ -71,7 +71,8 @@ private val DEFAULT_ACCOUNT = AccountJson(
email = "test@bitwarden.com",
isEmailVerified = true,
name = "Bitwarden Tester",
hasPremium = false,
hasPremiumPersonally = false,
hasPremiumFromOrganization = null,
stamp = null,
organizationId = null,
avatarColorHex = null,
@@ -95,7 +96,8 @@ private val UPDATED_ACCOUNT = AccountJson(
email = "test@bitwarden.com",
isEmailVerified = true,
name = "Bitwarden Tester",
hasPremium = false,
hasPremiumPersonally = false,
hasPremiumFromOrganization = null,
stamp = null,
organizationId = null,
avatarColorHex = null,
@@ -7916,7 +7916,8 @@ class AuthRepositoryTest {
email = EMAIL,
isEmailVerified = true,
name = "Bitwarden Tester",
hasPremium = false,
hasPremiumPersonally = false,
hasPremiumFromOrganization = null,
stamp = null,
organizationId = null,
avatarColorHex = null,
@@ -7954,7 +7955,8 @@ class AuthRepositoryTest {
email = EMAIL_2,
isEmailVerified = true,
name = "Bitwarden Tester 2",
hasPremium = false,
hasPremiumPersonally = false,
hasPremiumFromOrganization = null,
stamp = null,
organizationId = null,
avatarColorHex = null,
@@ -482,7 +482,8 @@ private val MOCK_PROFILE = AccountJson.Profile(
stamp = null,
organizationId = null,
avatarColorHex = null,
hasPremium = false,
hasPremiumPersonally = false,
hasPremiumFromOrganization = null,
forcePasswordResetReason = null,
kdfType = null,
kdfIterations = null,
@@ -134,7 +134,8 @@ private val PROFILE_1 = AccountJson.Profile(
email = "test@bitwarden.com",
isEmailVerified = true,
name = "Bitwarden Tester",
hasPremium = false,
hasPremiumPersonally = false,
hasPremiumFromOrganization = null,
stamp = null,
organizationId = null,
avatarColorHex = null,
@@ -159,7 +160,8 @@ private val ACCOUNT_2 = AccountJson(
email = "test2@bitwarden.com",
isEmailVerified = true,
name = "Bitwarden Tester 2",
hasPremium = false,
hasPremiumPersonally = false,
hasPremiumFromOrganization = null,
stamp = null,
organizationId = null,
avatarColorHex = null,
@@ -36,6 +36,8 @@ import io.mockk.mockkStatic
import io.mockk.unmockkStatic
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import java.time.Instant
@@ -77,7 +79,8 @@ class UserStateJsonExtensionsTest {
stamp = null,
organizationId = null,
avatarColorHex = null,
hasPremium = true,
hasPremiumPersonally = true,
hasPremiumFromOrganization = null,
forcePasswordResetReason = null,
kdfType = KdfTypeJson.ARGON2_ID,
kdfIterations = 600000,
@@ -127,7 +130,8 @@ class UserStateJsonExtensionsTest {
stamp = null,
organizationId = null,
avatarColorHex = null,
hasPremium = true,
hasPremiumPersonally = true,
hasPremiumFromOrganization = null,
forcePasswordResetReason = null,
kdfType = KdfTypeJson.ARGON2_ID,
kdfIterations = 600000,
@@ -193,6 +197,147 @@ class UserStateJsonExtensionsTest {
)
}
@Suppress("MaxLineLength")
@Test
fun `toUpdatedUserStateJson should store personal and organization-granted premium separately`() {
val originalProfile = AccountJson.Profile(
userId = "activeUserId",
email = "email",
isEmailVerified = true,
name = "name",
stamp = "stamp",
organizationId = null,
avatarColorHex = "color",
hasPremiumPersonally = false,
hasPremiumFromOrganization = false,
forcePasswordResetReason = null,
kdfType = KdfTypeJson.ARGON2_ID,
kdfIterations = 600000,
kdfMemory = 16,
kdfParallelism = 4,
userDecryptionOptions = null,
isTwoFactorEnabled = false,
creationDate = null,
)
val originalAccount = AccountJson(
profile = originalProfile,
tokens = mockk(),
settings = mockk(),
)
val originalState = UserStateJson(
activeUserId = "activeUserId",
accounts = mapOf("activeUserId" to originalAccount),
)
val orgOnlyResult = originalState.toUpdatedUserStateJson(
syncResponse = mockk {
every { profile } returns mockk {
every { id } returns "activeUserId"
every { avatarColor } returns "color"
every { securityStamp } returns "stamp"
every { isPremium } returns false
every { isPremiumFromOrganization } returns true
every { isTwoFactorEnabled } returns false
every { creationDate } returns Instant.parse("2024-09-13T01:00:00.00Z")
every { userDecryption } returns null
}
},
)
val orgOnlyProfile = orgOnlyResult.accounts.getValue("activeUserId").profile
assertEquals(false, orgOnlyProfile.hasPremiumPersonally)
assertEquals(true, orgOnlyProfile.hasPremiumFromOrganization)
val personalOnlyResult = originalState.toUpdatedUserStateJson(
syncResponse = mockk {
every { profile } returns mockk {
every { id } returns "activeUserId"
every { avatarColor } returns "color"
every { securityStamp } returns "stamp"
every { isPremium } returns true
every { isPremiumFromOrganization } returns false
every { isTwoFactorEnabled } returns false
every { creationDate } returns Instant.parse("2024-09-13T01:00:00.00Z")
every { userDecryption } returns null
}
},
)
val personalOnlyProfile = personalOnlyResult.accounts.getValue("activeUserId").profile
assertEquals(true, personalOnlyProfile.hasPremiumPersonally)
assertEquals(false, personalOnlyProfile.hasPremiumFromOrganization)
}
@Suppress("MaxLineLength")
@Test
fun `toUserState should derive aggregate isPremium and isPremiumFromSelf from profile premium fields`() {
val baseProfile = AccountJson.Profile(
userId = "activeUserId",
email = "email",
isEmailVerified = true,
name = "name",
stamp = "stamp",
organizationId = null,
avatarColorHex = "color",
hasPremiumPersonally = false,
hasPremiumFromOrganization = false,
forcePasswordResetReason = null,
kdfType = KdfTypeJson.ARGON2_ID,
kdfIterations = 600000,
kdfMemory = 16,
kdfParallelism = 4,
userDecryptionOptions = null,
isTwoFactorEnabled = false,
creationDate = null,
)
fun stateWith(hasPremiumPersonally: Boolean?, hasPremiumFromOrg: Boolean?): UserStateJson =
UserStateJson(
activeUserId = "activeUserId",
accounts = mapOf(
"activeUserId" to AccountJson(
profile = baseProfile.copy(
hasPremiumPersonally = hasPremiumPersonally,
hasPremiumFromOrganization = hasPremiumFromOrg,
),
tokens = null,
settings = AccountJson.Settings(environmentUrlData = null),
),
),
)
fun toAccount(state: UserStateJson) = state
.toUserState(
vaultState = emptyList(),
userAccountTokens = emptyList(),
userOrganizationsList = emptyList(),
userIsUsingKeyConnectorList = emptyList(),
hasPendingAccountAddition = false,
onboardingStatus = OnboardingStatus.COMPLETE,
firstTimeState = FirstTimeState(),
isBiometricsEnabledProvider = { false },
vaultUnlockTypeProvider = { VaultUnlockType.MASTER_PASSWORD },
isDeviceTrustedProvider = { false },
getUserPolicies = { _, _ -> emptyList() },
)
.accounts
.first()
val freeAccount = toAccount(stateWith(hasPremiumPersonally = false, hasPremiumFromOrg = false))
assertFalse(freeAccount.isPremium)
assertFalse(freeAccount.isPremiumFromSelf)
val personalAccount = toAccount(stateWith(hasPremiumPersonally = true, hasPremiumFromOrg = false))
assertTrue(personalAccount.isPremium)
assertTrue(personalAccount.isPremiumFromSelf)
val orgOnlyAccount = toAccount(stateWith(hasPremiumPersonally = false, hasPremiumFromOrg = true))
assertTrue(orgOnlyAccount.isPremium)
assertFalse(orgOnlyAccount.isPremiumFromSelf)
val bothAccount = toAccount(stateWith(hasPremiumPersonally = true, hasPremiumFromOrg = true))
assertTrue(bothAccount.isPremium)
assertTrue(bothAccount.isPremiumFromSelf)
}
@Test
fun `toUpdatedUserStateJson should update the correct account with new information`() {
val originalProfile = AccountJson.Profile(
@@ -203,7 +348,8 @@ class UserStateJsonExtensionsTest {
stamp = null,
organizationId = null,
avatarColorHex = null,
hasPremium = true,
hasPremiumPersonally = true,
hasPremiumFromOrganization = null,
forcePasswordResetReason = null,
kdfType = KdfTypeJson.ARGON2_ID,
kdfIterations = 600000,
@@ -226,6 +372,7 @@ class UserStateJsonExtensionsTest {
profile = originalProfile.copy(
avatarColorHex = "avatarColor",
stamp = "securityStamp",
hasPremiumFromOrganization = true,
isTwoFactorEnabled = false,
creationDate = Instant.parse("2024-09-13T01:00:00.00Z"),
),
@@ -266,7 +413,8 @@ class UserStateJsonExtensionsTest {
stamp = null,
organizationId = null,
avatarColorHex = null,
hasPremium = true,
hasPremiumPersonally = true,
hasPremiumFromOrganization = null,
forcePasswordResetReason = ForcePasswordResetReason
.TDE_USER_WITHOUT_PASSWORD_HAS_PASSWORD_RESET_PERMISSION,
kdfType = KdfTypeJson.ARGON2_ID,
@@ -320,7 +468,8 @@ class UserStateJsonExtensionsTest {
stamp = null,
organizationId = null,
avatarColorHex = null,
hasPremium = true,
hasPremiumPersonally = true,
hasPremiumFromOrganization = null,
forcePasswordResetReason = ForcePasswordResetReason
.TDE_USER_WITHOUT_PASSWORD_HAS_PASSWORD_RESET_PERMISSION,
kdfType = KdfTypeJson.ARGON2_ID,
@@ -393,7 +542,8 @@ class UserStateJsonExtensionsTest {
stamp = null,
organizationId = null,
avatarColorHex = null,
hasPremium = true,
hasPremiumPersonally = true,
hasPremiumFromOrganization = null,
forcePasswordResetReason = null,
kdfType = KdfTypeJson.ARGON2_ID,
kdfIterations = 600000,
@@ -453,6 +603,7 @@ class UserStateJsonExtensionsTest {
avatarColorHex = "activeAvatarColorHex",
environment = Environment.Eu,
isPremium = false,
isPremiumFromSelf = false,
isLoggedIn = true,
isVaultUnlocked = true,
needsPasswordReset = false,
@@ -486,7 +637,8 @@ class UserStateJsonExtensionsTest {
every { name } returns "activeName"
every { email } returns "activeEmail"
every { avatarColorHex } returns "activeAvatarColorHex"
every { hasPremium } returns null
every { hasPremiumPersonally } returns null
every { hasPremiumFromOrganization } returns null
every { forcePasswordResetReason } returns null
every { userDecryptionOptions } returns UserDecryptionOptionsJson(
hasMasterPassword = true,
@@ -564,6 +716,7 @@ class UserStateJsonExtensionsTest {
avatarColorHex = "#ffecbc49",
environment = Environment.Eu,
isPremium = true,
isPremiumFromSelf = true,
isLoggedIn = false,
isVaultUnlocked = false,
needsPasswordReset = false,
@@ -598,7 +751,8 @@ class UserStateJsonExtensionsTest {
every { name } returns "activeName"
every { email } returns "activeEmail"
every { avatarColorHex } returns null
every { hasPremium } returns true
every { hasPremiumPersonally } returns true
every { hasPremiumFromOrganization } returns null
every { forcePasswordResetReason } returns null
every { userDecryptionOptions } returns UserDecryptionOptionsJson(
hasMasterPassword = false,
@@ -672,6 +826,7 @@ class UserStateJsonExtensionsTest {
avatarColorHex = "#ffecbc49",
environment = Environment.Eu,
isPremium = true,
isPremiumFromSelf = true,
isLoggedIn = false,
isVaultUnlocked = false,
needsPasswordReset = false,
@@ -711,7 +866,8 @@ class UserStateJsonExtensionsTest {
every { name } returns "activeName"
every { email } returns "activeEmail"
every { avatarColorHex } returns null
every { hasPremium } returns true
every { hasPremiumPersonally } returns true
every { hasPremiumFromOrganization } returns null
every { forcePasswordResetReason } returns null
every { userDecryptionOptions } returns UserDecryptionOptionsJson(
hasMasterPassword = false,
@@ -788,6 +944,7 @@ class UserStateJsonExtensionsTest {
avatarColorHex = "#ffecbc49",
environment = Environment.Eu,
isPremium = true,
isPremiumFromSelf = true,
isLoggedIn = false,
isVaultUnlocked = false,
needsPasswordReset = false,
@@ -827,7 +984,8 @@ class UserStateJsonExtensionsTest {
every { name } returns "activeName"
every { email } returns "activeEmail"
every { avatarColorHex } returns null
every { hasPremium } returns true
every { hasPremiumPersonally } returns true
every { hasPremiumFromOrganization } returns null
every { forcePasswordResetReason } returns null
every { userDecryptionOptions } returns UserDecryptionOptionsJson(
hasMasterPassword = false,
@@ -904,6 +1062,7 @@ class UserStateJsonExtensionsTest {
avatarColorHex = "#ffecbc49",
environment = Environment.Eu,
isPremium = true,
isPremiumFromSelf = true,
isLoggedIn = false,
isVaultUnlocked = false,
needsPasswordReset = false,
@@ -943,7 +1102,8 @@ class UserStateJsonExtensionsTest {
every { name } returns "activeName"
every { email } returns "activeEmail"
every { avatarColorHex } returns null
every { hasPremium } returns true
every { hasPremiumPersonally } returns true
every { hasPremiumFromOrganization } returns null
every { forcePasswordResetReason } returns null
every { userDecryptionOptions } returns UserDecryptionOptionsJson(
hasMasterPassword = false,
@@ -1020,6 +1180,7 @@ class UserStateJsonExtensionsTest {
avatarColorHex = "#ffecbc49",
environment = Environment.Eu,
isPremium = true,
isPremiumFromSelf = true,
isLoggedIn = false,
isVaultUnlocked = false,
needsPasswordReset = false,
@@ -1066,7 +1227,8 @@ class UserStateJsonExtensionsTest {
every { name } returns "activeName"
every { email } returns "activeEmail"
every { avatarColorHex } returns null
every { hasPremium } returns true
every { hasPremiumPersonally } returns true
every { hasPremiumFromOrganization } returns null
every { forcePasswordResetReason } returns null
every { userDecryptionOptions } returns UserDecryptionOptionsJson(
hasMasterPassword = false,
@@ -1143,6 +1305,7 @@ class UserStateJsonExtensionsTest {
avatarColorHex = "#ffecbc49",
environment = Environment.Eu,
isPremium = false,
isPremiumFromSelf = false,
isLoggedIn = false,
isVaultUnlocked = false,
needsPasswordReset = false,
@@ -1170,7 +1333,8 @@ class UserStateJsonExtensionsTest {
every { name } returns "activeName"
every { email } returns "activeEmail"
every { avatarColorHex } returns null
every { hasPremium } returns false
every { hasPremiumPersonally } returns false
every { hasPremiumFromOrganization } returns null
every { forcePasswordResetReason } returns null
// The decryption options are what are determining the result
@Suppress("MaxLineLength")
@@ -1226,6 +1390,7 @@ class UserStateJsonExtensionsTest {
avatarColorHex = "#ffecbc49",
environment = Environment.Eu,
isPremium = false,
isPremiumFromSelf = false,
isLoggedIn = false,
isVaultUnlocked = false,
needsPasswordReset = false,
@@ -1253,7 +1418,8 @@ class UserStateJsonExtensionsTest {
every { name } returns "activeName"
every { email } returns "activeEmail"
every { avatarColorHex } returns null
every { hasPremium } returns false
every { hasPremiumPersonally } returns false
every { hasPremiumFromOrganization } returns null
every { forcePasswordResetReason } returns null
// The decryption options are what are determining the result
every { userDecryptionOptions } returns UserDecryptionOptionsJson(
@@ -1312,6 +1478,7 @@ class UserStateJsonExtensionsTest {
avatarColorHex = "#ffecbc49",
environment = Environment.Eu,
isPremium = false,
isPremiumFromSelf = false,
isLoggedIn = false,
isVaultUnlocked = false,
needsPasswordReset = false,
@@ -1352,7 +1519,8 @@ class UserStateJsonExtensionsTest {
every { name } returns "activeName"
every { email } returns "activeEmail"
every { avatarColorHex } returns null
every { hasPremium } returns false
every { hasPremiumPersonally } returns false
every { hasPremiumFromOrganization } returns null
every { forcePasswordResetReason } returns null
// The decryption options are what are determining the result
@Suppress("MaxLineLength")
@@ -1432,6 +1600,7 @@ class UserStateJsonExtensionsTest {
avatarColorHex = "#ffecbc49",
environment = Environment.Eu,
isPremium = true,
isPremiumFromSelf = true,
isLoggedIn = false,
isVaultUnlocked = false,
needsPasswordReset = false,
@@ -1473,7 +1642,8 @@ class UserStateJsonExtensionsTest {
every { name } returns "activeName"
every { email } returns "activeEmail"
every { avatarColorHex } returns null
every { hasPremium } returns true
every { hasPremiumPersonally } returns true
every { hasPremiumFromOrganization } returns null
every { forcePasswordResetReason } returns null
every { userDecryptionOptions } returns UserDecryptionOptionsJson(
hasMasterPassword = false,
@@ -1550,6 +1720,7 @@ class UserStateJsonExtensionsTest {
avatarColorHex = "activeAvatarColorHex",
environment = Environment.Eu,
isPremium = false,
isPremiumFromSelf = false,
isLoggedIn = true,
isVaultUnlocked = true,
needsPasswordReset = false,
@@ -1583,7 +1754,8 @@ class UserStateJsonExtensionsTest {
every { name } returns "activeName"
every { email } returns "activeEmail"
every { avatarColorHex } returns "activeAvatarColorHex"
every { hasPremium } returns null
every { hasPremiumPersonally } returns null
every { hasPremiumFromOrganization } returns null
every { forcePasswordResetReason } returns null
every { userDecryptionOptions } returns UserDecryptionOptionsJson(
hasMasterPassword = true,
@@ -1670,6 +1842,7 @@ class UserStateJsonExtensionsTest {
avatarColorHex = "activeAvatarColorHex",
environment = Environment.Eu,
isPremium = false,
isPremiumFromSelf = false,
isLoggedIn = true,
isVaultUnlocked = true,
needsPasswordReset = false,
@@ -1703,7 +1876,8 @@ class UserStateJsonExtensionsTest {
every { name } returns "activeName"
every { email } returns "activeEmail"
every { avatarColorHex } returns "activeAvatarColorHex"
every { hasPremium } returns null
every { hasPremiumPersonally } returns null
every { hasPremiumFromOrganization } returns null
every { forcePasswordResetReason } returns null
every { userDecryptionOptions } returns UserDecryptionOptionsJson(
hasMasterPassword = true,
@@ -1788,7 +1962,8 @@ class UserStateJsonExtensionsTest {
stamp = null,
organizationId = null,
avatarColorHex = null,
hasPremium = true,
hasPremiumPersonally = true,
hasPremiumFromOrganization = null,
forcePasswordResetReason = null,
kdfType = KdfTypeJson.ARGON2_ID,
kdfIterations = 600000,
@@ -1831,7 +2006,8 @@ class UserStateJsonExtensionsTest {
profile = originalProfile.copy(
avatarColorHex = "avatarColor",
stamp = "securityStamp",
hasPremium = false,
hasPremiumPersonally = false,
hasPremiumFromOrganization = false,
isTwoFactorEnabled = true,
creationDate = Instant.parse("2024-09-13T01:00:00.00Z"),
kdfType = KdfTypeJson.PBKDF2_SHA256,
@@ -1870,7 +2046,8 @@ class UserStateJsonExtensionsTest {
stamp = null,
organizationId = null,
avatarColorHex = null,
hasPremium = true,
hasPremiumPersonally = true,
hasPremiumFromOrganization = null,
forcePasswordResetReason = null,
kdfType = KdfTypeJson.ARGON2_ID,
kdfIterations = 600000,
@@ -1918,7 +2095,8 @@ class UserStateJsonExtensionsTest {
profile = originalProfile.copy(
avatarColorHex = "newAvatarColor",
stamp = "newSecurityStamp",
hasPremium = true,
hasPremiumPersonally = true,
hasPremiumFromOrganization = false,
isTwoFactorEnabled = true,
creationDate = Instant.parse("2024-09-13T01:00:00.00Z"),
kdfType = KdfTypeJson.PBKDF2_SHA256,
@@ -1951,7 +2129,8 @@ class UserStateJsonExtensionsTest {
stamp = null,
organizationId = null,
avatarColorHex = null,
hasPremium = true,
hasPremiumPersonally = true,
hasPremiumFromOrganization = null,
forcePasswordResetReason = null,
kdfType = KdfTypeJson.ARGON2_ID,
kdfIterations = 600000,
@@ -1997,7 +2176,8 @@ class UserStateJsonExtensionsTest {
profile = originalProfile.copy(
avatarColorHex = "updatedAvatarColor",
stamp = "updatedSecurityStamp",
hasPremium = true,
hasPremiumPersonally = false,
hasPremiumFromOrganization = true,
isTwoFactorEnabled = false,
creationDate = Instant.parse("2024-09-13T01:00:00.00Z"),
userDecryptionOptions = UserDecryptionOptionsJson(
@@ -2025,7 +2205,8 @@ class UserStateJsonExtensionsTest {
stamp = null,
organizationId = null,
avatarColorHex = null,
hasPremium = false,
hasPremiumPersonally = false,
hasPremiumFromOrganization = null,
forcePasswordResetReason = null,
kdfType = KdfTypeJson.PBKDF2_SHA256,
kdfIterations = 100_000,
@@ -2078,6 +2259,7 @@ class UserStateJsonExtensionsTest {
"activeUserId" to originalAccount.copy(
profile = originalProfile.copy(
kdfIterations = DEFAULT_PBKDF2_ITERATIONS,
hasPremiumFromOrganization = false,
userDecryptionOptions = UserDecryptionOptionsJson(
hasMasterPassword = true,
masterPasswordUnlock = MasterPasswordUnlockDataJson(
@@ -2111,7 +2293,8 @@ class UserStateJsonExtensionsTest {
stamp = "stamp",
organizationId = null,
avatarColorHex = "avatarColorHex",
hasPremium = true,
hasPremiumPersonally = true,
hasPremiumFromOrganization = null,
forcePasswordResetReason = null,
kdfType = KdfTypeJson.ARGON2_ID,
kdfIterations = 600000,
@@ -2168,7 +2351,8 @@ class UserStateJsonExtensionsTest {
stamp = "securityStamp",
organizationId = "orgId",
avatarColorHex = "#FF0000",
hasPremium = false,
hasPremiumPersonally = false,
hasPremiumFromOrganization = null,
forcePasswordResetReason = null,
kdfType = KdfTypeJson.ARGON2_ID,
kdfIterations = 100000,
@@ -2220,7 +2404,8 @@ class UserStateJsonExtensionsTest {
stamp = null,
organizationId = null,
avatarColorHex = null,
hasPremium = true,
hasPremiumPersonally = true,
hasPremiumFromOrganization = null,
forcePasswordResetReason = null,
kdfType = KdfTypeJson.ARGON2_ID,
kdfIterations = 600000,
@@ -2238,7 +2423,8 @@ class UserStateJsonExtensionsTest {
stamp = null,
organizationId = null,
avatarColorHex = null,
hasPremium = false,
hasPremiumPersonally = false,
hasPremiumFromOrganization = null,
forcePasswordResetReason = null,
kdfType = KdfTypeJson.ARGON2_ID,
kdfIterations = 500000,
@@ -392,6 +392,108 @@ class PremiumStateManagerImplTest {
)
}
@Test
fun `isPlanRowEligibleFlow emits true for a free user when feature flag is enabled`() =
runTest {
mutableUserStateFlow.value = DEFAULT_USER_STATE.copy(
accounts = listOf(
DEFAULT_ACTIVE_ACCOUNT.copy(
isPremium = false,
isPremiumFromSelf = false,
),
),
)
val manager = createManager()
manager.isPlanRowEligibleFlow.test {
assertTrue(awaitItem())
}
}
@Test
fun `isPlanRowEligibleFlow emits true for a personal Premium user`() = runTest {
mutableUserStateFlow.value = DEFAULT_USER_STATE.copy(
accounts = listOf(
DEFAULT_ACTIVE_ACCOUNT.copy(
isPremium = true,
isPremiumFromSelf = true,
),
),
)
val manager = createManager()
manager.isPlanRowEligibleFlow.test {
assertTrue(awaitItem())
}
}
@Test
fun `isPlanRowEligibleFlow emits false for a user with only org-granted Premium`() =
runTest {
mutableUserStateFlow.value = DEFAULT_USER_STATE.copy(
accounts = listOf(
DEFAULT_ACTIVE_ACCOUNT.copy(
isPremium = true,
isPremiumFromSelf = false,
),
),
)
val manager = createManager()
manager.isPlanRowEligibleFlow.test {
assertFalse(awaitItem())
}
}
@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 {
mutableUserStateFlow.value = null
val manager = createManager()
manager.isPlanRowEligibleFlow.test {
assertFalse(awaitItem())
}
}
@Test
fun `isPlanRowEligibleFlow updates as the user gains and loses org-only Premium`() = runTest {
mutableUserStateFlow.value = DEFAULT_USER_STATE.copy(
accounts = listOf(
DEFAULT_ACTIVE_ACCOUNT.copy(
isPremium = false,
isPremiumFromSelf = false,
),
),
)
val manager = createManager()
manager.isPlanRowEligibleFlow.test {
assertTrue(awaitItem())
mutableUserStateFlow.value = DEFAULT_USER_STATE.copy(
accounts = listOf(
DEFAULT_ACTIVE_ACCOUNT.copy(
isPremium = true,
isPremiumFromSelf = false,
),
),
)
assertFalse(awaitItem())
mutableUserStateFlow.value = DEFAULT_USER_STATE.copy(
accounts = listOf(
DEFAULT_ACTIVE_ACCOUNT.copy(
isPremium = false,
isPremiumFromSelf = false,
),
),
)
assertTrue(awaitItem())
}
}
@Test
fun `isUpgradedToPremiumCardEligibleFlow emits false when nothing has been observed`() =
runTest {
@@ -404,6 +506,16 @@ class PremiumStateManagerImplTest {
@Test
fun `premium-status push with isPremium=true marks the card pending and emits true`() =
runTest {
// Sync has caught up to the upgrade by the time the push lands — the active user
// already holds personal Premium.
mutableUserStateFlow.value = DEFAULT_USER_STATE.copy(
accounts = listOf(
DEFAULT_ACTIVE_ACCOUNT.copy(
isPremium = true,
isPremiumFromSelf = true,
),
),
)
val manager = createManager()
manager.isUpgradedToPremiumCardEligibleFlow.test {
assertFalse(awaitItem())
@@ -421,6 +533,32 @@ class PremiumStateManagerImplTest {
}
}
@Suppress("MaxLineLength")
@Test
fun `isUpgradedToPremiumCardEligibleFlow emits false when pending is set but the active user lacks personal Premium`() =
runTest {
// Simulates the debug-menu trigger (or a stray PREMIUM_STATUS_CHANGED push routed to
// an organization-granted user): pending is written directly to disk, but the active
// user has no personal subscription. The read-side gate must withhold the card.
mutableUserStateFlow.value = DEFAULT_USER_STATE.copy(
accounts = listOf(
DEFAULT_ACTIVE_ACCOUNT.copy(
isPremium = true,
isPremiumFromSelf = false,
),
),
)
fakeSettingsDiskSource.storeUpgradedToPremiumCardPending(
userId = ACTIVE_USER_ID,
isPending = true,
)
val manager = createManager()
manager.isUpgradedToPremiumCardEligibleFlow.test {
assertFalse(awaitItem())
expectNoEvents()
}
}
@Test
fun `premium-status push with isPremium=false should not mark the card pending`() =
runTest {
@@ -441,19 +579,30 @@ class PremiumStateManagerImplTest {
}
}
@Suppress("MaxLineLength")
@Test
fun `userState transition from non-Premium to Premium for the same user marks card pending`() =
fun `userState transition from non-Premium to personal Premium for the same user marks card pending`() =
runTest {
// Start as Free.
mutableUserStateFlow.value = DEFAULT_USER_STATE.copy(
accounts = listOf(DEFAULT_ACTIVE_ACCOUNT.copy(isPremium = false)),
accounts = listOf(
DEFAULT_ACTIVE_ACCOUNT.copy(
isPremium = false,
isPremiumFromSelf = false,
),
),
)
val manager = createManager()
manager.isUpgradedToPremiumCardEligibleFlow.test {
assertFalse(awaitItem())
// Transition to Premium for the same user.
// Transition to personal Premium for the same user.
mutableUserStateFlow.value = DEFAULT_USER_STATE.copy(
accounts = listOf(DEFAULT_ACTIVE_ACCOUNT.copy(isPremium = true)),
accounts = listOf(
DEFAULT_ACTIVE_ACCOUNT.copy(
isPremium = true,
isPremiumFromSelf = true,
),
),
)
assertTrue(awaitItem())
fakeSettingsDiskSource.assertUpgradedToPremiumCardPending(
@@ -463,6 +612,38 @@ class PremiumStateManagerImplTest {
}
}
@Test
fun `userState transition that only flips org-granted Premium should not mark card pending`() =
runTest {
mutableUserStateFlow.value = DEFAULT_USER_STATE.copy(
accounts = listOf(
DEFAULT_ACTIVE_ACCOUNT.copy(
isPremium = false,
isPremiumFromSelf = false,
),
),
)
val manager = createManager()
manager.isUpgradedToPremiumCardEligibleFlow.test {
assertFalse(awaitItem())
// Aggregate isPremium becomes true via the user's organization, but personal
// premium stays false — the card must not arm.
mutableUserStateFlow.value = DEFAULT_USER_STATE.copy(
accounts = listOf(
DEFAULT_ACTIVE_ACCOUNT.copy(
isPremium = true,
isPremiumFromSelf = false,
),
),
)
expectNoEvents()
fakeSettingsDiskSource.assertUpgradedToPremiumCardPending(
userId = ACTIVE_USER_ID,
expected = null,
)
}
}
@Test
fun `Premium account that signs in for the first time should not mark the card pending`() =
runTest {
@@ -472,7 +653,12 @@ class PremiumStateManagerImplTest {
manager.isUpgradedToPremiumCardEligibleFlow.test {
assertFalse(awaitItem())
mutableUserStateFlow.value = DEFAULT_USER_STATE.copy(
accounts = listOf(DEFAULT_ACTIVE_ACCOUNT.copy(isPremium = true)),
accounts = listOf(
DEFAULT_ACTIVE_ACCOUNT.copy(
isPremium = true,
isPremiumFromSelf = true,
),
),
)
expectNoEvents()
fakeSettingsDiskSource.assertUpgradedToPremiumCardPending(
@@ -562,6 +748,7 @@ private val DEFAULT_ACTIVE_ACCOUNT = UserState.Account(
avatarColorHex = "#aa00aa",
environment = com.bitwarden.data.repository.model.Environment.Us,
isPremium = false,
isPremiumFromSelf = false,
isLoggedIn = true,
isVaultUnlocked = true,
needsPasswordReset = false,
@@ -693,6 +693,7 @@ private fun createMockAccounts(number: Int): List<UserState.Account> {
avatarColorHex = "$it",
environment = Environment.Us,
isPremium = true,
isPremiumFromSelf = true,
isLoggedIn = true,
isVaultUnlocked = true,
needsPasswordReset = false,
@@ -429,7 +429,8 @@ private val MOCK_PROFILE = AccountJson.Profile(
email = "test@bitwarden.com",
isEmailVerified = true,
name = "Bitwarden Tester",
hasPremium = false,
hasPremiumPersonally = false,
hasPremiumFromOrganization = null,
stamp = null,
organizationId = null,
avatarColorHex = null,
@@ -1496,7 +1496,8 @@ private val MOCK_PROFILE = AccountJson.Profile(
email = "test@bitwarden.com",
isEmailVerified = true,
name = "Bitwarden Tester",
hasPremium = false,
hasPremiumPersonally = false,
hasPremiumFromOrganization = null,
stamp = null,
organizationId = null,
avatarColorHex = null,
@@ -785,7 +785,8 @@ private val USER_STATE = UserStateJson(
stamp = "stamp",
organizationId = "organizationId",
avatarColorHex = "avatarColorHex",
hasPremium = true,
hasPremiumPersonally = true,
hasPremiumFromOrganization = null,
forcePasswordResetReason = ForcePasswordResetReason.ADMIN_FORCE_PASSWORD_RESET,
kdfType = KdfTypeJson.ARGON2_ID,
kdfIterations = 600000,
@@ -3069,7 +3069,8 @@ private val MOCK_PROFILE = AccountJson.Profile(
stamp = "mockSecurityStamp-1",
organizationId = null,
avatarColorHex = null,
hasPremium = false,
hasPremiumPersonally = false,
hasPremiumFromOrganization = null,
forcePasswordResetReason = null,
kdfType = null,
kdfIterations = null,
@@ -620,7 +620,8 @@ private val MOCK_PROFILE = AccountJson.Profile(
stamp = "mockSecurityStamp-1",
organizationId = null,
avatarColorHex = null,
hasPremium = false,
hasPremiumPersonally = false,
hasPremiumFromOrganization = null,
forcePasswordResetReason = null,
kdfType = null,
kdfIterations = null,
@@ -996,7 +996,8 @@ private val MOCK_PROFILE = AccountJson.Profile(
stamp = "mockSecurityStamp-1",
organizationId = null,
avatarColorHex = null,
hasPremium = false,
hasPremiumPersonally = false,
hasPremiumFromOrganization = null,
forcePasswordResetReason = null,
kdfType = null,
kdfIterations = null,
@@ -2132,7 +2132,8 @@ private val MOCK_PROFILE = AccountJson.Profile(
stamp = null,
organizationId = null,
avatarColorHex = null,
hasPremium = false,
hasPremiumPersonally = false,
hasPremiumFromOrganization = null,
forcePasswordResetReason = null,
kdfType = null,
kdfIterations = null,
@@ -1213,7 +1213,8 @@ private val MOCK_USER_STATE = UserStateJson(
stamp = null,
organizationId = null,
avatarColorHex = null,
hasPremium = false,
hasPremiumPersonally = false,
hasPremiumFromOrganization = null,
forcePasswordResetReason = null,
kdfType = null,
kdfIterations = null,
@@ -731,6 +731,7 @@ class VaultSyncManagerTest {
profile = MOCK_PROFILE.copy(
avatarColorHex = "mockAvatarColor-1",
stamp = "mockSecurityStamp-1",
hasPremiumFromOrganization = false,
kdfType = KdfTypeJson.PBKDF2_SHA256,
kdfIterations = 600000,
kdfMemory = null,
@@ -1321,7 +1322,8 @@ private val MOCK_PROFILE = AccountJson.Profile(
stamp = "mockSecurityStamp-1",
organizationId = null,
avatarColorHex = null,
hasPremium = false,
hasPremiumPersonally = false,
hasPremiumFromOrganization = null,
forcePasswordResetReason = null,
kdfType = null,
kdfIterations = null,
@@ -1743,7 +1743,8 @@ private val MOCK_BASE_PROFILE = AccountJson.Profile(
stamp = "mockSecurityStamp-1",
organizationId = null,
avatarColorHex = null,
hasPremium = false,
hasPremiumPersonally = false,
hasPremiumFromOrganization = null,
forcePasswordResetReason = null,
kdfType = null,
kdfIterations = null,
@@ -20,7 +20,7 @@ fun createMockAccountJsonProfile(
stamp: String = "mockSecurityStamp-$number",
organizationId: String? = "mockOrganizationId-$number",
avatarColorHex: String? = "mockAvatarColorHex-$number",
hasPremium: Boolean = false,
hasPremiumPersonally: Boolean = false,
forcePasswordResetReason: ForcePasswordResetReason? = null,
kdfType: KdfTypeJson? = null,
kdfIterations: Int? = null,
@@ -37,7 +37,8 @@ fun createMockAccountJsonProfile(
stamp = stamp,
organizationId = organizationId,
avatarColorHex = avatarColorHex,
hasPremium = hasPremium,
hasPremiumPersonally = hasPremiumPersonally,
hasPremiumFromOrganization = null,
forcePasswordResetReason = forcePasswordResetReason,
kdfType = kdfType,
kdfIterations = kdfIterations,
@@ -423,6 +423,7 @@ private val DEFAULT_USER_ACCOUNT = UserState.Account(
avatarColorHex = "#aa00aa",
environment = Environment.Us,
isPremium = true,
isPremiumFromSelf = true,
isLoggedIn = true,
isVaultUnlocked = true,
needsPasswordReset = false,
@@ -84,6 +84,7 @@ class LandingViewModelTest : BaseViewModelTest() {
avatarColorHex = "avatarColorHex",
environment = Environment.Us,
isPremium = true,
isPremiumFromSelf = true,
isLoggedIn = true,
isVaultUnlocked = true,
needsPasswordReset = false,
@@ -238,6 +239,7 @@ class LandingViewModelTest : BaseViewModelTest() {
avatarColorHex = "avatarColorHex",
environment = Environment.Us,
isPremium = true,
isPremiumFromSelf = true,
isLoggedIn = true,
isVaultUnlocked = true,
needsPasswordReset = false,
@@ -297,6 +299,7 @@ class LandingViewModelTest : BaseViewModelTest() {
avatarColorHex = "avatarColorHex",
environment = Environment.Us,
isPremium = true,
isPremiumFromSelf = true,
isLoggedIn = true,
isVaultUnlocked = true,
needsPasswordReset = false,
@@ -360,6 +363,7 @@ class LandingViewModelTest : BaseViewModelTest() {
avatarColorHex = "avatarColorHex",
environment = Environment.Us,
isPremium = true,
isPremiumFromSelf = true,
isLoggedIn = false,
isVaultUnlocked = true,
needsPasswordReset = false,
@@ -524,6 +528,7 @@ class LandingViewModelTest : BaseViewModelTest() {
avatarColorHex = "lorem",
environment = Environment.Us,
isPremium = false,
isPremiumFromSelf = false,
isLoggedIn = false,
isVaultUnlocked = false,
needsPasswordReset = false,
@@ -562,6 +567,7 @@ class LandingViewModelTest : BaseViewModelTest() {
avatarColorHex = "lorem",
environment = Environment.Us,
isPremium = false,
isPremiumFromSelf = false,
isLoggedIn = false,
isVaultUnlocked = false,
needsPasswordReset = false,
@@ -121,6 +121,7 @@ class LoginViewModelTest : BaseViewModelTest() {
avatarColorHex = "avatarColorHex",
environment = Environment.Us,
isPremium = true,
isPremiumFromSelf = true,
isLoggedIn = true,
isVaultUnlocked = true,
needsPasswordReset = false,
@@ -284,6 +284,7 @@ private val DEFAULT_ACCOUNT = UserState.Account(
environment = Environment.Us,
avatarColorHex = "#aa00aa",
isPremium = true,
isPremiumFromSelf = true,
isLoggedIn = true,
isVaultUnlocked = true,
needsPasswordReset = false,
@@ -282,6 +282,7 @@ private val DEFAULT_ACCOUNT = UserState.Account(
environment = Environment.Us,
avatarColorHex = "#aa00aa",
isPremium = true,
isPremiumFromSelf = true,
isLoggedIn = true,
isVaultUnlocked = true,
needsPasswordReset = false,
@@ -260,6 +260,7 @@ class VaultUnlockViewModelTest : BaseViewModelTest() {
avatarColorHex = "#00aaaa",
environment = Environment.Us,
isPremium = true,
isPremiumFromSelf = true,
isLoggedIn = true,
isVaultUnlocked = true,
needsPasswordReset = false,
@@ -302,6 +303,7 @@ class VaultUnlockViewModelTest : BaseViewModelTest() {
avatarColorHex = "#00aaaa",
environment = Environment.Us,
isPremium = true,
isPremiumFromSelf = true,
isLoggedIn = true,
isVaultUnlocked = false,
needsPasswordReset = false,
@@ -1406,6 +1408,7 @@ private val DEFAULT_ACCOUNT = UserState.Account(
environment = Environment.Us,
avatarColorHex = "#aa00aa",
isPremium = true,
isPremiumFromSelf = true,
isLoggedIn = true,
isVaultUnlocked = true,
needsPasswordReset = false,
@@ -1314,6 +1314,7 @@ private val DEFAULT_ACCOUNT = UserState.Account(
avatarColorHex = "#000000",
environment = mockk(),
isPremium = false,
isPremiumFromSelf = false,
isLoggedIn = true,
isVaultUnlocked = true,
needsPasswordReset = false,
@@ -115,6 +115,7 @@ class RootNavViewModelTest : BaseViewModelTest() {
avatarColorHex = "avatarColorHex",
environment = Environment.Us,
isPremium = true,
isPremiumFromSelf = true,
isLoggedIn = false,
isVaultUnlocked = true,
needsPasswordReset = false,
@@ -154,6 +155,7 @@ class RootNavViewModelTest : BaseViewModelTest() {
avatarColorHex = "avatarColorHex",
environment = Environment.Us,
isPremium = true,
isPremiumFromSelf = true,
isLoggedIn = false,
isVaultUnlocked = false,
needsPasswordReset = true,
@@ -190,6 +192,7 @@ class RootNavViewModelTest : BaseViewModelTest() {
avatarColorHex = "avatarColorHex",
environment = Environment.Us,
isPremium = true,
isPremiumFromSelf = true,
isLoggedIn = false,
isVaultUnlocked = false,
needsPasswordReset = true,
@@ -231,6 +234,7 @@ class RootNavViewModelTest : BaseViewModelTest() {
avatarColorHex = "avatarColorHex",
environment = Environment.Us,
isPremium = true,
isPremiumFromSelf = true,
isLoggedIn = false,
isVaultUnlocked = false,
needsPasswordReset = false,
@@ -274,6 +278,7 @@ class RootNavViewModelTest : BaseViewModelTest() {
avatarColorHex = "avatarColorHex",
environment = Environment.Us,
isPremium = true,
isPremiumFromSelf = true,
isLoggedIn = true,
isVaultUnlocked = false,
needsPasswordReset = false,
@@ -317,6 +322,7 @@ class RootNavViewModelTest : BaseViewModelTest() {
avatarColorHex = "avatarColorHex",
environment = Environment.Us,
isPremium = true,
isPremiumFromSelf = true,
isLoggedIn = false,
isVaultUnlocked = true,
needsPasswordReset = false,
@@ -362,6 +368,7 @@ class RootNavViewModelTest : BaseViewModelTest() {
avatarColorHex = "avatarColorHex",
environment = Environment.Us,
isPremium = true,
isPremiumFromSelf = true,
isLoggedIn = true,
isVaultUnlocked = true,
needsPasswordReset = false,
@@ -407,6 +414,7 @@ class RootNavViewModelTest : BaseViewModelTest() {
avatarColorHex = "avatarColorHex",
environment = Environment.Us,
isPremium = true,
isPremiumFromSelf = true,
isLoggedIn = true,
isVaultUnlocked = true,
needsPasswordReset = false,
@@ -453,6 +461,7 @@ class RootNavViewModelTest : BaseViewModelTest() {
avatarColorHex = "avatarColorHex",
environment = Environment.Us,
isPremium = true,
isPremiumFromSelf = true,
isLoggedIn = true,
isVaultUnlocked = true,
needsPasswordReset = false,
@@ -495,6 +504,7 @@ class RootNavViewModelTest : BaseViewModelTest() {
avatarColorHex = "avatarColorHex",
environment = Environment.Us,
isPremium = true,
isPremiumFromSelf = true,
isLoggedIn = true,
isVaultUnlocked = true,
needsPasswordReset = false,
@@ -537,6 +547,7 @@ class RootNavViewModelTest : BaseViewModelTest() {
avatarColorHex = "avatarColorHex",
environment = Environment.Us,
isPremium = true,
isPremiumFromSelf = true,
isLoggedIn = true,
isVaultUnlocked = true,
needsPasswordReset = false,
@@ -580,6 +591,7 @@ class RootNavViewModelTest : BaseViewModelTest() {
avatarColorHex = "avatarColorHex",
environment = Environment.Us,
isPremium = true,
isPremiumFromSelf = true,
isLoggedIn = true,
isVaultUnlocked = true,
needsPasswordReset = false,
@@ -625,6 +637,7 @@ class RootNavViewModelTest : BaseViewModelTest() {
avatarColorHex = "avatarColorHex",
environment = Environment.Us,
isPremium = true,
isPremiumFromSelf = true,
isLoggedIn = true,
isVaultUnlocked = true,
needsPasswordReset = false,
@@ -677,6 +690,7 @@ class RootNavViewModelTest : BaseViewModelTest() {
avatarColorHex = "avatarColorHex",
environment = Environment.Us,
isPremium = true,
isPremiumFromSelf = true,
isLoggedIn = true,
isVaultUnlocked = true,
needsPasswordReset = false,
@@ -734,6 +748,7 @@ class RootNavViewModelTest : BaseViewModelTest() {
avatarColorHex = "avatarHexColor",
environment = Environment.Us,
isPremium = true,
isPremiumFromSelf = true,
isLoggedIn = true,
isVaultUnlocked = true,
needsPasswordReset = false,
@@ -801,6 +816,7 @@ class RootNavViewModelTest : BaseViewModelTest() {
avatarColorHex = "avatarHexColor",
environment = Environment.Us,
isPremium = true,
isPremiumFromSelf = true,
isLoggedIn = true,
isVaultUnlocked = true,
needsPasswordReset = false,
@@ -849,6 +865,7 @@ class RootNavViewModelTest : BaseViewModelTest() {
avatarColorHex = "avatarHexColor",
environment = Environment.Us,
isPremium = true,
isPremiumFromSelf = true,
isLoggedIn = true,
isVaultUnlocked = true,
needsPasswordReset = false,
@@ -896,6 +913,7 @@ class RootNavViewModelTest : BaseViewModelTest() {
avatarColorHex = "avatarHexColor",
environment = Environment.Us,
isPremium = true,
isPremiumFromSelf = true,
isLoggedIn = true,
isVaultUnlocked = true,
needsPasswordReset = false,
@@ -1033,6 +1051,7 @@ class RootNavViewModelTest : BaseViewModelTest() {
avatarColorHex = "avatarHexColor",
environment = Environment.Us,
isPremium = true,
isPremiumFromSelf = true,
isLoggedIn = true,
isVaultUnlocked = true,
needsPasswordReset = false,
@@ -1089,6 +1108,7 @@ class RootNavViewModelTest : BaseViewModelTest() {
avatarColorHex = "avatarColorHex",
environment = Environment.Us,
isPremium = true,
isPremiumFromSelf = true,
isLoggedIn = true,
isVaultUnlocked = false,
needsPasswordReset = false,
@@ -1153,6 +1173,7 @@ class RootNavViewModelTest : BaseViewModelTest() {
avatarColorHex = "avatarHexColor",
environment = Environment.Us,
isPremium = true,
isPremiumFromSelf = true,
isLoggedIn = true,
isVaultUnlocked = true,
needsPasswordReset = false,
@@ -1197,6 +1218,7 @@ class RootNavViewModelTest : BaseViewModelTest() {
avatarColorHex = "avatarColorHex",
environment = Environment.Us,
isPremium = true,
isPremiumFromSelf = true,
isLoggedIn = true,
isVaultUnlocked = false,
needsPasswordReset = false,
@@ -1236,6 +1258,7 @@ class RootNavViewModelTest : BaseViewModelTest() {
avatarColorHex = "avatarColorHex",
environment = Environment.Us,
isPremium = true,
isPremiumFromSelf = true,
isLoggedIn = true,
isVaultUnlocked = false,
needsPasswordReset = false,
@@ -1273,6 +1296,7 @@ class RootNavViewModelTest : BaseViewModelTest() {
avatarColorHex = "avatarColorHex",
environment = Environment.Us,
isPremium = true,
isPremiumFromSelf = true,
isLoggedIn = true,
isVaultUnlocked = true,
needsPasswordReset = false,
@@ -1313,6 +1337,7 @@ class RootNavViewModelTest : BaseViewModelTest() {
avatarColorHex = "avatarColorHex",
environment = Environment.Us,
isPremium = true,
isPremiumFromSelf = true,
isLoggedIn = true,
isVaultUnlocked = true,
needsPasswordReset = false,
@@ -1353,6 +1378,7 @@ class RootNavViewModelTest : BaseViewModelTest() {
avatarColorHex = "avatarColorHex",
environment = Environment.Us,
isPremium = true,
isPremiumFromSelf = true,
isLoggedIn = true,
isVaultUnlocked = true,
needsPasswordReset = false,
@@ -1393,6 +1419,7 @@ class RootNavViewModelTest : BaseViewModelTest() {
avatarColorHex = "avatarColorHex",
environment = Environment.Us,
isPremium = true,
isPremiumFromSelf = true,
isLoggedIn = true,
isVaultUnlocked = true,
needsPasswordReset = false,
@@ -1433,6 +1460,7 @@ class RootNavViewModelTest : BaseViewModelTest() {
avatarColorHex = "avatarColorHex",
environment = Environment.Us,
isPremium = true,
isPremiumFromSelf = true,
isLoggedIn = true,
isVaultUnlocked = true,
needsPasswordReset = false,
@@ -1473,6 +1501,7 @@ class RootNavViewModelTest : BaseViewModelTest() {
avatarColorHex = "avatarColorHex",
environment = Environment.Us,
isPremium = true,
isPremiumFromSelf = true,
isLoggedIn = true,
isVaultUnlocked = false,
needsPasswordReset = false,
@@ -1513,6 +1542,7 @@ class RootNavViewModelTest : BaseViewModelTest() {
avatarColorHex = "avatarColorHex",
environment = Environment.Us,
isPremium = true,
isPremiumFromSelf = true,
isLoggedIn = true,
isVaultUnlocked = true,
needsPasswordReset = false,
@@ -1553,6 +1583,7 @@ class RootNavViewModelTest : BaseViewModelTest() {
avatarColorHex = "avatarColorHex",
environment = Environment.Us,
isPremium = true,
isPremiumFromSelf = true,
isLoggedIn = true,
isVaultUnlocked = true,
needsPasswordReset = false,
@@ -1759,6 +1790,7 @@ private val MOCK_VAULT_UNLOCKED_USER_STATE = UserState(
avatarColorHex = "avatarColorHex",
environment = Environment.Us,
isPremium = true,
isPremiumFromSelf = true,
isLoggedIn = true,
isVaultUnlocked = true,
needsPasswordReset = false,
@@ -1786,6 +1818,7 @@ private val MOCK_VAULT_UNLOCKED_USER_MULTIPLE_ACCOUNTS_STATE = UserState(
avatarColorHex = "avatarColorHex",
environment = Environment.Us,
isPremium = true,
isPremiumFromSelf = true,
isLoggedIn = true,
isVaultUnlocked = true,
needsPasswordReset = false,
@@ -1808,6 +1841,7 @@ private val MOCK_VAULT_UNLOCKED_USER_MULTIPLE_ACCOUNTS_STATE = UserState(
avatarColorHex = "avatarColorHex",
environment = Environment.Us,
isPremium = true,
isPremiumFromSelf = true,
isLoggedIn = true,
isVaultUnlocked = true,
needsPasswordReset = false,
@@ -2393,6 +2393,7 @@ private val DEFAULT_ACCOUNT = UserState.Account(
avatarColorHex = "#aa00aa",
environment = Environment.Us,
isPremium = true,
isPremiumFromSelf = true,
isLoggedIn = true,
isVaultUnlocked = true,
needsPasswordReset = false,
@@ -305,4 +305,5 @@ private val DEFAULT_STATE = SettingsState(
securityCount = 0,
autoFillCount = 0,
vaultCount = 0,
isPlanRowEligible = false,
)
@@ -2,10 +2,8 @@ package com.x8bit.bitwarden.ui.platform.feature.settings
import androidx.lifecycle.SavedStateHandle
import app.cash.turbine.test
import com.bitwarden.core.data.manager.model.FlagKey
import com.bitwarden.ui.platform.base.BaseViewModelTest
import com.x8bit.bitwarden.data.billing.manager.PremiumStateManager
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManager
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager
import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance
@@ -31,16 +29,7 @@ class SettingsViewModelTest : BaseViewModelTest() {
private val mutableAutofillBadgeCountFlow = MutableStateFlow(0)
private val mutableVaultBadgeCountFlow = MutableStateFlow(0)
private val mutableSecurityBadgeCountFlow = MutableStateFlow(0)
private val mutableMobilePremiumUpgradeFlagFlow = MutableStateFlow(false)
private val featureFlagManager = mockk<FeatureFlagManager> {
every {
getFeatureFlag(FlagKey.MobilePremiumUpgrade)
} answers { mutableMobilePremiumUpgradeFlagFlow.value }
every {
getFeatureFlagFlow(FlagKey.MobilePremiumUpgrade)
} returns mutableMobilePremiumUpgradeFlagFlow
}
private val firstTimeManager = mockk<FirstTimeActionManager> {
every { allSecuritySettingsBadgeCountFlow } returns mutableSecurityBadgeCountFlow
every { allAutofillSettingsBadgeCountFlow } returns mutableAutofillBadgeCountFlow
@@ -50,8 +39,10 @@ class SettingsViewModelTest : BaseViewModelTest() {
every { specialCircumstance } returns null
}
private val mutablePlanRowEligibleFlow = MutableStateFlow(false)
private val mutableUpgradedToPremiumCardEligibleFlow = MutableStateFlow(false)
private val premiumStateManager: PremiumStateManager = mockk(relaxed = true) {
every { isPlanRowEligibleFlow } returns mutablePlanRowEligibleFlow
every {
isUpgradedToPremiumCardEligibleFlow
} returns mutableUpgradedToPremiumCardEligibleFlow
@@ -220,11 +211,8 @@ class SettingsViewModelTest : BaseViewModelTest() {
}
@Test
fun `Plan row should appear when feature flag is enabled and not preAuth`() {
every {
featureFlagManager.getFeatureFlag(FlagKey.MobilePremiumUpgrade)
} returns true
mutableMobilePremiumUpgradeFlagFlow.value = true
fun `Plan row should appear when plan row eligibility is true and not preAuth`() {
mutablePlanRowEligibleFlow.value = true
val viewModel = createViewModel()
assertTrue(
viewModel.stateFlow.value.settingRows
@@ -233,10 +221,8 @@ class SettingsViewModelTest : BaseViewModelTest() {
}
@Test
fun `Plan row should be hidden when feature flag is disabled`() {
every {
featureFlagManager.getFeatureFlag(FlagKey.MobilePremiumUpgrade)
} returns false
fun `Plan row should be hidden when plan row eligibility is false`() {
mutablePlanRowEligibleFlow.value = false
val viewModel = createViewModel()
assertFalse(
viewModel.stateFlow.value.settingRows
@@ -246,10 +232,7 @@ class SettingsViewModelTest : BaseViewModelTest() {
@Test
fun `Plan row should be hidden in preAuth mode`() {
every {
featureFlagManager.getFeatureFlag(FlagKey.MobilePremiumUpgrade)
} returns true
mutableMobilePremiumUpgradeFlagFlow.value = true
mutablePlanRowEligibleFlow.value = true
val viewModel = createViewModel(isPreAuth = true)
assertFalse(
viewModel.stateFlow.value.settingRows
@@ -259,10 +242,7 @@ class SettingsViewModelTest : BaseViewModelTest() {
@Test
fun `Plan row should appear between Appearance and Other in settings rows`() {
every {
featureFlagManager.getFeatureFlag(FlagKey.MobilePremiumUpgrade)
} returns true
mutableMobilePremiumUpgradeFlagFlow.value = true
mutablePlanRowEligibleFlow.value = true
val viewModel = createViewModel()
val rows = viewModel.stateFlow.value.settingRows
val planIndex = rows.indexOf(Settings.PLAN)
@@ -273,20 +253,16 @@ class SettingsViewModelTest : BaseViewModelTest() {
}
@Test
fun `Plan row should update when feature flag changes to enabled`() =
fun `Plan row should update when plan row eligibility changes to true`() =
runTest {
every {
featureFlagManager.getFeatureFlag(
FlagKey.MobilePremiumUpgrade,
)
} returns false
mutablePlanRowEligibleFlow.value = false
val viewModel = createViewModel()
assertFalse(
viewModel.stateFlow.value.settingRows
.contains(Settings.PLAN),
)
mutableMobilePremiumUpgradeFlagFlow.value = true
mutablePlanRowEligibleFlow.value = true
viewModel.stateFlow.test {
assertTrue(
awaitItem().settingRows.contains(Settings.PLAN),
@@ -349,7 +325,6 @@ class SettingsViewModelTest : BaseViewModelTest() {
private fun createViewModel(isPreAuth: Boolean = false) = SettingsViewModel(
firstTimeActionManager = firstTimeManager,
featureFlagManager = featureFlagManager,
specialCircumstanceManager = specialCircumstanceManager,
premiumStateManager = premiumStateManager,
savedStateHandle = SavedStateHandle().apply {
@@ -363,4 +338,5 @@ private val DEFAULT_STATE = SettingsState(
autoFillCount = 0,
securityCount = 0,
vaultCount = 0,
isPlanRowEligible = false,
)
@@ -909,6 +909,7 @@ private val DEFAULT_USER_STATE = UserState(
avatarColorHex = "#aa00aa",
environment = Environment.Us,
isPremium = true,
isPremiumFromSelf = true,
isLoggedIn = true,
isVaultUnlocked = true,
needsPasswordReset = false,
@@ -232,6 +232,7 @@ private val DEFAULT_USER_STATE: UserState = UserState(
avatarColorHex = "avatarColorHex",
environment = Environment.Us,
isPremium = true,
isPremiumFromSelf = true,
isLoggedIn = true,
isVaultUnlocked = true,
needsPasswordReset = false,
@@ -485,6 +485,7 @@ private val DEFAULT_USER_STATE = UserState(
avatarColorHex = "#aa00aa",
isBiometricsEnabled = false,
isPremium = true,
isPremiumFromSelf = true,
isLoggedIn = true,
isVaultUnlocked = true,
needsPasswordReset = false,
@@ -506,6 +507,7 @@ private val DEFAULT_USER_STATE = UserState(
avatarColorHex = "#aa00aa",
isBiometricsEnabled = false,
isPremium = true,
isPremiumFromSelf = true,
isLoggedIn = true,
isVaultUnlocked = true,
needsPasswordReset = false,
@@ -844,6 +844,7 @@ private val DEFAULT_USER_STATE = UserState(
avatarColorHex = "#aa00aa",
environment = Environment.Us,
isPremium = true,
isPremiumFromSelf = true,
isLoggedIn = true,
isVaultUnlocked = true,
needsPasswordReset = false,
@@ -2697,6 +2697,7 @@ private val DEFAULT_USER_STATE = UserState(
environment = Environment.Us,
avatarColorHex = "#aa00aa",
isPremium = true,
isPremiumFromSelf = true,
isLoggedIn = true,
isVaultUnlocked = true,
needsPasswordReset = false,
@@ -782,6 +782,7 @@ private val DEFAULT_USER_ACCOUNT_STATE = UserState.Account(
avatarColorHex = "#ff00ff",
environment = Environment.Us,
isPremium = false,
isPremiumFromSelf = false,
isLoggedIn = true,
isVaultUnlocked = true,
needsPasswordReset = false,
@@ -1568,6 +1568,7 @@ private val DEFAULT_ACCOUNT = UserState.Account(
environment = Environment.Us,
avatarColorHex = "#aa00aa",
isPremium = true,
isPremiumFromSelf = true,
isLoggedIn = true,
isVaultUnlocked = true,
needsPasswordReset = false,
@@ -6557,6 +6557,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
avatarColorHex = "#ffecbc49",
environment = Environment.Eu,
isPremium = true,
isPremiumFromSelf = true,
isLoggedIn = false,
isVaultUnlocked = false,
needsPasswordReset = false,
@@ -751,6 +751,7 @@ class CipherViewExtensionsTest {
avatarColorHex = "#ffecbc49",
environment = Environment.Eu,
isPremium = true,
isPremiumFromSelf = true,
isLoggedIn = false,
isVaultUnlocked = false,
needsPasswordReset = false,
@@ -820,6 +820,7 @@ private val DEFAULT_ACCOUNT = UserState.Account(
environment = Environment.Us,
avatarColorHex = "#aa00aa",
isPremium = true,
isPremiumFromSelf = true,
isLoggedIn = true,
isVaultUnlocked = true,
needsPasswordReset = false,
@@ -283,6 +283,7 @@ private val DEFAULT_ACCOUNT = UserState.Account(
environment = Environment.Us,
avatarColorHex = "#FF00FF00",
isPremium = true,
isPremiumFromSelf = true,
isLoggedIn = true,
isVaultUnlocked = true,
needsPasswordReset = false,
@@ -444,6 +444,7 @@ private val DEFAULT_USER_STATE = UserState(
avatarColorHex = "#aa00aa",
environment = Environment.Us,
isPremium = true,
isPremiumFromSelf = true,
isLoggedIn = true,
isVaultUnlocked = true,
needsPasswordReset = false,
@@ -241,6 +241,7 @@ private val DEFAULT_USER_STATE = UserState(
avatarColorHex = "#aa00aa",
environment = Environment.Us,
isPremium = true,
isPremiumFromSelf = true,
isLoggedIn = true,
isVaultUnlocked = true,
needsPasswordReset = false,
@@ -726,6 +726,7 @@ private val DEFAULT_USER_STATE = UserState(
avatarColorHex = "#aa00aa",
environment = Environment.Us,
isPremium = true,
isPremiumFromSelf = true,
isLoggedIn = true,
isVaultUnlocked = true,
needsPasswordReset = false,
@@ -756,6 +757,7 @@ private val DEFAULT_USER_STATE = UserState(
avatarColorHex = "#aa00aa",
environment = Environment.Us,
isPremium = true,
isPremiumFromSelf = true,
isLoggedIn = true,
isVaultUnlocked = true,
needsPasswordReset = false,
@@ -3839,6 +3839,7 @@ class VaultItemViewModelTest : BaseViewModelTest() {
avatarColorHex = "#ff00ff",
environment = Environment.Us,
isPremium = true,
isPremiumFromSelf = true,
isLoggedIn = true,
isVaultUnlocked = true,
needsPasswordReset = false,
@@ -6670,6 +6670,7 @@ private val DEFAULT_ACCOUNT = UserState.Account(
environment = Environment.Us,
avatarColorHex = "#aa00aa",
isPremium = true,
isPremiumFromSelf = true,
isLoggedIn = true,
isVaultUnlocked = true,
needsPasswordReset = false,
@@ -268,6 +268,7 @@ private val DEFAULT_USER_STATE = UserState(
avatarColorHex = "#175DDC",
environment = Environment.Us,
isPremium = false,
isPremiumFromSelf = false,
isLoggedIn = true,
isVaultUnlocked = true,
needsPasswordReset = false,
@@ -588,6 +588,7 @@ private val DEFAULT_USER_STATE = UserState(
avatarColorHex = "#aa00aa",
environment = Environment.Us,
isPremium = true,
isPremiumFromSelf = true,
isLoggedIn = true,
isVaultUnlocked = true,
needsPasswordReset = false,
@@ -94,6 +94,7 @@ private fun createMockUserState(hasOrganizations: Boolean = true): UserState =
avatarColorHex = "#aa00aa",
environment = Environment.Us,
isPremium = true,
isPremiumFromSelf = true,
isLoggedIn = true,
isVaultUnlocked = true,
needsPasswordReset = false,
@@ -615,6 +615,7 @@ class VaultViewModelTest : BaseViewModelTest() {
avatarColorHex = "#00aaaa",
environment = Environment.Us,
isPremium = true,
isPremiumFromSelf = true,
isLoggedIn = true,
isVaultUnlocked = true,
needsPasswordReset = false,
@@ -702,6 +703,7 @@ class VaultViewModelTest : BaseViewModelTest() {
avatarColorHex = "#00aaaa",
environment = Environment.Us,
isPremium = true,
isPremiumFromSelf = true,
isLoggedIn = true,
isVaultUnlocked = true,
needsPasswordReset = false,
@@ -4535,6 +4537,7 @@ private val DEFAULT_ACTIVE_ACCOUNT = UserState.Account(
avatarColorHex = "#aa00aa",
environment = Environment.Us,
isPremium = true,
isPremiumFromSelf = true,
isLoggedIn = true,
isVaultUnlocked = true,
needsPasswordReset = false,
@@ -4557,6 +4560,7 @@ private val DEFAULT_INACTIVE_ACCOUNT = UserState.Account(
avatarColorHex = "#00aaaa",
environment = Environment.Us,
isPremium = false,
isPremiumFromSelf = false,
isLoggedIn = true,
isVaultUnlocked = false,
needsPasswordReset = false,
@@ -69,6 +69,7 @@ class UserStateExtensionsTest {
avatarColorHex = "activeAvatarColorHex",
environment = Environment.Us,
isPremium = true,
isPremiumFromSelf = true,
isLoggedIn = true,
isVaultUnlocked = true,
needsPasswordReset = false,
@@ -97,6 +98,7 @@ class UserStateExtensionsTest {
avatarColorHex = "lockedAvatarColorHex",
environment = Environment.Eu,
isPremium = false,
isPremiumFromSelf = false,
isLoggedIn = true,
isVaultUnlocked = false,
needsPasswordReset = false,
@@ -129,6 +131,7 @@ class UserStateExtensionsTest {
),
),
isPremium = true,
isPremiumFromSelf = true,
isLoggedIn = true,
isVaultUnlocked = true,
needsPasswordReset = false,
@@ -161,6 +164,7 @@ class UserStateExtensionsTest {
),
),
isPremium = true,
isPremiumFromSelf = true,
isLoggedIn = false,
isVaultUnlocked = false,
needsPasswordReset = false,
@@ -208,6 +212,7 @@ class UserStateExtensionsTest {
avatarColorHex = "avatarColorHex",
environment = Environment.Us,
isPremium = true,
isPremiumFromSelf = true,
isLoggedIn = true,
isVaultUnlocked = true,
needsPasswordReset = false,
@@ -253,6 +258,7 @@ class UserStateExtensionsTest {
avatarColorHex = "avatarColorHex",
environment = Environment.Us,
isPremium = false,
isPremiumFromSelf = false,
isLoggedIn = true,
isVaultUnlocked = false,
needsPasswordReset = false,
@@ -302,6 +308,7 @@ class UserStateExtensionsTest {
avatarColorHex = "avatarColorHex",
environment = Environment.Us,
isPremium = true,
isPremiumFromSelf = true,
isLoggedIn = true,
isVaultUnlocked = true,
needsPasswordReset = false,
@@ -339,6 +346,7 @@ class UserStateExtensionsTest {
avatarColorHex = "avatarColorHex",
environment = Environment.Us,
isPremium = true,
isPremiumFromSelf = true,
isLoggedIn = true,
isVaultUnlocked = true,
needsPasswordReset = false,
@@ -383,6 +391,7 @@ class UserStateExtensionsTest {
avatarColorHex = "avatarColorHex",
environment = Environment.Us,
isPremium = true,
isPremiumFromSelf = true,
isLoggedIn = true,
isVaultUnlocked = true,
needsPasswordReset = false,
@@ -441,6 +450,7 @@ class UserStateExtensionsTest {
avatarColorHex = "avatarColorHex",
environment = Environment.Us,
isPremium = true,
isPremiumFromSelf = true,
isLoggedIn = true,
isVaultUnlocked = true,
needsPasswordReset = false,