diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/billing/di/BillingModule.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/billing/di/BillingModule.kt index 41bb945ffe..78d66bca45 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/billing/di/BillingModule.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/billing/di/BillingModule.kt @@ -13,6 +13,7 @@ import com.x8bit.bitwarden.data.billing.repository.BillingRepositoryImpl import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager import com.x8bit.bitwarden.data.platform.manager.PushManager +import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository import com.x8bit.bitwarden.data.vault.repository.VaultRepository import dagger.Module import dagger.Provides @@ -57,6 +58,7 @@ object BillingModule { settingsDiskSource: SettingsDiskSource, vaultRepository: VaultRepository, featureFlagManager: FeatureFlagManager, + environmentRepository: EnvironmentRepository, pushManager: PushManager, clock: Clock, dispatcherManager: DispatcherManager, @@ -66,6 +68,7 @@ object BillingModule { settingsDiskSource = settingsDiskSource, vaultRepository = vaultRepository, featureFlagManager = featureFlagManager, + environmentRepository = environmentRepository, pushManager = pushManager, clock = clock, dispatcherManager = dispatcherManager, diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/billing/manager/PremiumStateManager.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/billing/manager/PremiumStateManager.kt index cee77c5b43..c652d43c37 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/billing/manager/PremiumStateManager.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/billing/manager/PremiumStateManager.kt @@ -38,6 +38,18 @@ interface PremiumStateManager { */ val subscriptionStatusStateFlow: StateFlow + /** + * Emits whether the current state should be treated as self-hosted for premium upgrade + * gating. Reactive equivalent of [isSelfHosted]. + */ + val isSelfHostedFlow: StateFlow + + /** + * `true` when the current state should be treated as self-hosted for premium upgrade + * gating, or `false` otherwise. + */ + val isSelfHosted: Boolean + /** * Returns `true` when the in-app upgrade flow is available, or `false` otherwise. */ diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/billing/manager/PremiumStateManagerImpl.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/billing/manager/PremiumStateManagerImpl.kt index fdec63a03f..2deecfe21a 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/billing/manager/PremiumStateManagerImpl.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/billing/manager/PremiumStateManagerImpl.kt @@ -3,6 +3,7 @@ package com.x8bit.bitwarden.data.billing.manager import com.bitwarden.core.data.manager.dispatcher.DispatcherManager 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.UserStateJson import com.x8bit.bitwarden.data.auth.repository.util.activeUserIdChangesFlow @@ -13,6 +14,7 @@ import com.x8bit.bitwarden.data.billing.repository.model.SubscriptionStatusState import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager import com.x8bit.bitwarden.data.platform.manager.PushManager +import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository import com.x8bit.bitwarden.data.platform.util.isActive import com.x8bit.bitwarden.data.platform.util.scanPairs import com.x8bit.bitwarden.data.vault.repository.VaultRepository @@ -47,6 +49,7 @@ class PremiumStateManagerImpl( private val settingsDiskSource: SettingsDiskSource, vaultRepository: VaultRepository, private val featureFlagManager: FeatureFlagManager, + private val environmentRepository: EnvironmentRepository, pushManager: PushManager, private val clock: Clock, dispatcherManager: DispatcherManager, @@ -140,6 +143,20 @@ class PremiumStateManagerImpl( initialValue = false, ) + override val isSelfHostedFlow: StateFlow = + combine( + environmentRepository.environmentStateFlow, + featureFlagManager.getFeatureFlagFlow(FlagKey.DebugDisableSelfHostPremiumCheck), + ) { environment, isDebugBypassEnabled -> + environment is Environment.SelfHosted && !isDebugBypassEnabled + } + .stateIn( + scope = unconfinedScope, + started = SharingStarted.Eagerly, + initialValue = environmentRepository.environment is Environment.SelfHosted && + !featureFlagManager.getFeatureFlag(FlagKey.DebugDisableSelfHostPremiumCheck), + ) + /** * 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 @@ -242,6 +259,8 @@ class PremiumStateManagerImpl( .launchIn(unconfinedScope) } + override val isSelfHosted: Boolean get() = isSelfHostedFlow.value + override fun isInAppUpgradeAvailable(): Boolean = billingRepository.isInAppBillingSupportedFlow.value && featureFlagManager.getFeatureFlag(FlagKey.MobilePremiumUpgrade) diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/premium/plan/PlanViewModel.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/premium/plan/PlanViewModel.kt index 6dbc8238c5..5798230e94 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/premium/plan/PlanViewModel.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/premium/plan/PlanViewModel.kt @@ -6,7 +6,6 @@ import androidx.annotation.StringRes import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope import com.bitwarden.core.data.util.toFormattedDateStyle -import com.bitwarden.data.repository.model.Environment import com.bitwarden.data.repository.util.baseWebVaultUrlOrDefault import com.bitwarden.ui.platform.base.BaseViewModel import com.bitwarden.ui.platform.manager.intent.model.AuthTabData @@ -82,7 +81,7 @@ class PlanViewModel @Inject constructor( ?.isPremium == true val showsPremiumView = isPremium || premiumStateManager.subscriptionStatusStateFlow.value.isPremiumViewEligible() - val isSelfHosted = environmentRepository.environment is Environment.SelfHosted + val isSelfHosted = premiumStateManager.isSelfHosted PlanState( planMode = planMode, viewState = when { diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/SettingsViewModel.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/SettingsViewModel.kt index c99e95f12a..445305bdaa 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/SettingsViewModel.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/SettingsViewModel.kt @@ -4,7 +4,6 @@ import androidx.annotation.DrawableRes import androidx.compose.material3.Text import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope -import com.bitwarden.data.repository.model.Environment import com.bitwarden.ui.platform.base.BaseViewModel import com.bitwarden.ui.platform.base.DeferredBackgroundEvent import com.bitwarden.ui.platform.resource.BitwardenDrawable @@ -16,7 +15,6 @@ import com.x8bit.bitwarden.data.billing.manager.UPGRADED_TO_PREMIUM_LEARN_MORE_U 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 -import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList @@ -34,7 +32,6 @@ import javax.inject.Inject class SettingsViewModel @Inject constructor( specialCircumstanceManager: SpecialCircumstanceManager, firstTimeActionManager: FirstTimeActionManager, - environmentRepository: EnvironmentRepository, private val premiumStateManager: PremiumStateManager, savedStateHandle: SavedStateHandle, ) : BaseViewModel( @@ -44,7 +41,7 @@ class SettingsViewModel @Inject constructor( autoFillCount = firstTimeActionManager.allAutofillSettingsBadgeCountFlow.value, vaultCount = firstTimeActionManager.allVaultSettingsBadgeCountFlow.value, isPlanRowEligible = premiumStateManager.isPlanRowEligibleFlow.value, - isSelfHosted = environmentRepository.environment is Environment.SelfHosted, + isSelfHosted = premiumStateManager.isSelfHostedFlow.value, isUpgradedToPremiumCardEligible = premiumStateManager .isUpgradedToPremiumCardEligibleFlow .value, @@ -80,13 +77,9 @@ class SettingsViewModel @Inject constructor( .onEach(::sendAction) .launchIn(viewModelScope) - environmentRepository - .environmentStateFlow - .map { - SettingsAction.Internal.EnvironmentReceive( - isSelfHosted = it is Environment.SelfHosted, - ) - } + premiumStateManager + .isSelfHostedFlow + .map { SettingsAction.Internal.SelfHostedStatusReceive(isSelfHosted = it) } .onEach(::sendAction) .launchIn(viewModelScope) @@ -117,13 +110,13 @@ class SettingsViewModel @Inject constructor( handleUpgradedToPremiumCardEligibilityReceive(action) } - is SettingsAction.Internal.EnvironmentReceive -> { - handleEnvironmentReceive(action) + is SettingsAction.Internal.SelfHostedStatusReceive -> { + handleSelfHostedStatusReceive(action) } } - private fun handleEnvironmentReceive( - action: SettingsAction.Internal.EnvironmentReceive, + private fun handleSelfHostedStatusReceive( + action: SettingsAction.Internal.SelfHostedStatusReceive, ) { mutableStateFlow.update { it.copy(isSelfHosted = action.isSelfHosted) @@ -364,9 +357,10 @@ sealed class SettingsAction { ) : Internal() /** - * Indicates that the environment has been updated. + * Indicates that the effective self-hosted status for premium gating has been updated — + * driven by environment changes or by the debug-only self-host-bypass flag. */ - data class EnvironmentReceive( + data class SelfHostedStatusReceive( val isSelfHosted: Boolean, ) : Internal() } diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/data/billing/manager/PremiumStateManagerTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/data/billing/manager/PremiumStateManagerTest.kt index e9694d2568..5bb9d0146f 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/data/billing/manager/PremiumStateManagerTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/data/billing/manager/PremiumStateManagerTest.kt @@ -5,6 +5,7 @@ import com.bitwarden.core.data.manager.dispatcher.FakeDispatcherManager import com.bitwarden.core.data.manager.model.FlagKey import com.bitwarden.core.data.repository.model.DataState import com.bitwarden.data.datasource.disk.model.EnvironmentUrlDataJson +import com.bitwarden.data.repository.model.Environment import com.bitwarden.vault.DecryptCipherListResult import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountJson import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson @@ -19,6 +20,7 @@ import com.x8bit.bitwarden.data.platform.datasource.disk.util.FakeSettingsDiskSo import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager import com.x8bit.bitwarden.data.platform.manager.PushManager import com.x8bit.bitwarden.data.platform.manager.model.PremiumStatusChangedData +import com.x8bit.bitwarden.data.platform.repository.util.FakeEnvironmentRepository import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCipherListView import com.x8bit.bitwarden.data.vault.repository.VaultRepository import com.x8bit.bitwarden.data.vault.repository.model.VaultData @@ -66,6 +68,7 @@ class PremiumStateManagerTest { } private val mutableMobilePremiumUpgradeFlagFlow = MutableStateFlow(true) + private val mutableDebugDisableSelfHostPremiumCheckFlagFlow = MutableStateFlow(false) private val featureFlagManager: FeatureFlagManager = mockk { every { getFeatureFlagFlow(FlagKey.MobilePremiumUpgrade) @@ -73,8 +76,16 @@ class PremiumStateManagerTest { every { getFeatureFlag(FlagKey.MobilePremiumUpgrade) } answers { mutableMobilePremiumUpgradeFlagFlow.value } + every { + getFeatureFlag(FlagKey.DebugDisableSelfHostPremiumCheck) + } answers { mutableDebugDisableSelfHostPremiumCheckFlagFlow.value } + every { + getFeatureFlagFlow(FlagKey.DebugDisableSelfHostPremiumCheck) + } returns mutableDebugDisableSelfHostPremiumCheckFlagFlow } + private val fakeEnvironmentRepository = FakeEnvironmentRepository() + private val dispatcherManager = FakeDispatcherManager() private val mutablePremiumStatusChangedFlow = @@ -89,6 +100,7 @@ class PremiumStateManagerTest { settingsDiskSource = fakeSettingsDiskSource, vaultRepository = vaultRepository, featureFlagManager = featureFlagManager, + environmentRepository = fakeEnvironmentRepository, pushManager = pushManager, clock = fixedClock, dispatcherManager = dispatcherManager, @@ -365,6 +377,75 @@ class PremiumStateManagerTest { assertFalse(manager.isInAppUpgradeAvailable()) } + @Test + fun `isSelfHosted should return false on cloud environment regardless of flag`() { + fakeEnvironmentRepository.environment = Environment.Us + val manager = createManager() + mutableDebugDisableSelfHostPremiumCheckFlagFlow.value = false + assertFalse(manager.isSelfHosted) + mutableDebugDisableSelfHostPremiumCheckFlagFlow.value = true + assertFalse(manager.isSelfHosted) + } + + @Test + fun `isSelfHosted should return true on self-hosted environment when flag is disabled`() { + fakeEnvironmentRepository.environment = Environment.SelfHosted( + environmentUrlData = EnvironmentUrlDataJson.DEFAULT_US, + ) + mutableDebugDisableSelfHostPremiumCheckFlagFlow.value = false + val manager = createManager() + assertTrue(manager.isSelfHosted) + } + + @Test + fun `isSelfHosted should return false on self-hosted environment when flag is enabled`() { + fakeEnvironmentRepository.environment = Environment.SelfHosted( + environmentUrlData = EnvironmentUrlDataJson.DEFAULT_US, + ) + mutableDebugDisableSelfHostPremiumCheckFlagFlow.value = true + val manager = createManager() + assertFalse(manager.isSelfHosted) + } + + @Test + fun `isSelfHostedFlow should emit false on cloud environment regardless of flag`() = runTest { + fakeEnvironmentRepository.environment = Environment.Us + val manager = createManager() + manager.isSelfHostedFlow.test { + assertFalse(awaitItem()) + mutableDebugDisableSelfHostPremiumCheckFlagFlow.value = true + expectNoEvents() + } + } + + @Test + fun `isSelfHostedFlow should emit true on self-hosted environment when flag is disabled`() = + runTest { + fakeEnvironmentRepository.environment = Environment.SelfHosted( + environmentUrlData = EnvironmentUrlDataJson.DEFAULT_US, + ) + val manager = createManager() + manager.isSelfHostedFlow.test { + assertTrue(awaitItem()) + } + } + + @Test + fun `isSelfHostedFlow should re-emit when debug-disable flag toggles on self-hosted env`() = + runTest { + fakeEnvironmentRepository.environment = Environment.SelfHosted( + environmentUrlData = EnvironmentUrlDataJson.DEFAULT_US, + ) + val manager = createManager() + manager.isSelfHostedFlow.test { + assertTrue(awaitItem()) + mutableDebugDisableSelfHostPremiumCheckFlagFlow.value = true + assertFalse(awaitItem()) + mutableDebugDisableSelfHostPremiumCheckFlagFlow.value = false + assertTrue(awaitItem()) + } + } + @Test fun `dismissPremiumUpgradeBanner should store dismissed state for active user`() { val manager = createManager() diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/premium/plan/PlanViewModelTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/premium/plan/PlanViewModelTest.kt index 39cccd18d3..9175781ce8 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/premium/plan/PlanViewModelTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/premium/plan/PlanViewModelTest.kt @@ -67,8 +67,10 @@ class PlanViewModelTest : BaseViewModelTest() { } private val mutableSubscriptionStatusStateFlow = MutableStateFlow(SubscriptionStatusState.NoSubscription) + private var mockIsSelfHosted = false private val mockPremiumStateManager: PremiumStateManager = mockk { every { subscriptionStatusStateFlow } returns mutableSubscriptionStatusStateFlow + every { isSelfHosted } answers { mockIsSelfHosted } } private val mutableEnvironmentFlow = MutableStateFlow(Environment.Us) private val mockEnvironmentRepository: EnvironmentRepository = mockk { @@ -612,6 +614,7 @@ class PlanViewModelTest : BaseViewModelTest() { @Test fun `initial state on self-hosted should be Free SelfHosted ViewState`() = runTest { + mockIsSelfHosted = true mutableEnvironmentFlow.value = Environment.SelfHosted( environmentUrlData = EnvironmentUrlDataJson.DEFAULT_US, ) @@ -633,6 +636,7 @@ class PlanViewModelTest : BaseViewModelTest() { @Test fun `initial state on self-hosted should not fetch pricing`() = runTest { + mockIsSelfHosted = true mutableEnvironmentFlow.value = Environment.SelfHosted( environmentUrlData = EnvironmentUrlDataJson.DEFAULT_US, ) @@ -643,6 +647,36 @@ class PlanViewModelTest : BaseViewModelTest() { } } + @Test + fun `self-hosted with debug disable flag enabled should show Free Cloud and fetch pricing`() = + runTest { + // The PremiumStateManager helper reports false when the debug-disable flag is on, + // so the view model treats the self-hosted env as cloud for premium-upgrade purposes. + mockIsSelfHosted = false + mutableEnvironmentFlow.value = Environment.SelfHosted( + environmentUrlData = EnvironmentUrlDataJson.DEFAULT_US, + ) + val viewModel = createViewModel() + + viewModel.stateFlow.test { + assertEquals( + PlanState( + planMode = PlanMode.Modal, + viewState = PlanState.ViewState.Free.Cloud( + rate = "$1.67", + checkoutUrl = null, + isAwaitingPremiumStatus = false, + ), + dialogState = null, + ), + awaitItem(), + ) + } + coVerify(exactly = 1) { + mockBillingRepository.getPremiumPlanPricing() + } + } + // endregion Self-hosted path // region Pricing fetch diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/SettingsViewModelTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/SettingsViewModelTest.kt index 11b6fa4869..6e99357fac 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/SettingsViewModelTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/SettingsViewModelTest.kt @@ -2,14 +2,11 @@ package com.x8bit.bitwarden.ui.platform.feature.settings import androidx.lifecycle.SavedStateHandle import app.cash.turbine.test -import com.bitwarden.data.datasource.disk.model.EnvironmentUrlDataJson -import com.bitwarden.data.repository.model.Environment import com.bitwarden.ui.platform.base.BaseViewModelTest import com.x8bit.bitwarden.data.billing.manager.PremiumStateManager 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 -import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository import io.mockk.every import io.mockk.just import io.mockk.mockk @@ -44,16 +41,13 @@ class SettingsViewModelTest : BaseViewModelTest() { private val mutablePlanRowEligibleFlow = MutableStateFlow(false) private val mutableUpgradedToPremiumCardEligibleFlow = MutableStateFlow(false) + private val mutableIsSelfHostedFlow = MutableStateFlow(false) private val premiumStateManager: PremiumStateManager = mockk(relaxed = true) { every { isPlanRowEligibleFlow } returns mutablePlanRowEligibleFlow every { isUpgradedToPremiumCardEligibleFlow } returns mutableUpgradedToPremiumCardEligibleFlow - } - private val mutableEnvironmentFlow = MutableStateFlow(Environment.Us) - private val environmentRepository: EnvironmentRepository = mockk { - every { environment } answers { mutableEnvironmentFlow.value } - every { environmentStateFlow } returns mutableEnvironmentFlow + every { isSelfHostedFlow } returns mutableIsSelfHostedFlow } @BeforeEach @@ -332,11 +326,9 @@ class SettingsViewModelTest : BaseViewModelTest() { } @Test - fun `Plan row should be hidden when environment is self-hosted`() { + fun `Plan row should be hidden when self-hosted status flow emits true`() { mutablePlanRowEligibleFlow.value = true - mutableEnvironmentFlow.value = Environment.SelfHosted( - environmentUrlData = EnvironmentUrlDataJson.DEFAULT_US, - ) + mutableIsSelfHostedFlow.value = true val viewModel = createViewModel() assertFalse( viewModel.stateFlow.value.settingRows @@ -345,7 +337,7 @@ class SettingsViewModelTest : BaseViewModelTest() { } @Test - fun `Plan row should update when environment changes to self-hosted`() = runTest { + fun `Plan row should update when self-hosted status flow flips to true`() = runTest { mutablePlanRowEligibleFlow.value = true val viewModel = createViewModel() assertTrue( @@ -353,9 +345,7 @@ class SettingsViewModelTest : BaseViewModelTest() { .contains(Settings.PLAN), ) - mutableEnvironmentFlow.value = Environment.SelfHosted( - environmentUrlData = EnvironmentUrlDataJson.DEFAULT_US, - ) + mutableIsSelfHostedFlow.value = true viewModel.stateFlow.test { assertFalse( awaitItem().settingRows.contains(Settings.PLAN), @@ -363,10 +353,29 @@ class SettingsViewModelTest : BaseViewModelTest() { } } + @Test + fun `Plan row should re-appear when self-hosted status flow flips back to false`() = runTest { + // Simulates the debug-disable flag being toggled on while self-hosted: the flow emits + // false even though the environment is still self-hosted. + mutablePlanRowEligibleFlow.value = true + mutableIsSelfHostedFlow.value = true + val viewModel = createViewModel() + assertFalse( + viewModel.stateFlow.value.settingRows + .contains(Settings.PLAN), + ) + + mutableIsSelfHostedFlow.value = false + viewModel.stateFlow.test { + assertTrue( + awaitItem().settingRows.contains(Settings.PLAN), + ) + } + } + private fun createViewModel(isPreAuth: Boolean = false) = SettingsViewModel( firstTimeActionManager = firstTimeManager, specialCircumstanceManager = specialCircumstanceManager, - environmentRepository = environmentRepository, premiumStateManager = premiumStateManager, savedStateHandle = SavedStateHandle().apply { every { toSettingsArgs() } returns SettingsArgs(isPreAuth = isPreAuth) diff --git a/core/src/main/kotlin/com/bitwarden/core/data/manager/model/FlagKey.kt b/core/src/main/kotlin/com/bitwarden/core/data/manager/model/FlagKey.kt index 7f1ae34b90..1b496f9937 100644 --- a/core/src/main/kotlin/com/bitwarden/core/data/manager/model/FlagKey.kt +++ b/core/src/main/kotlin/com/bitwarden/core/data/manager/model/FlagKey.kt @@ -45,6 +45,7 @@ sealed class FlagKey { V2EncryptionPassword, V2EncryptionTde, NewItemTypes, + DebugDisableSelfHostPremiumCheck, ) } } @@ -180,6 +181,16 @@ sealed class FlagKey { override val defaultValue: Boolean = false } + /** + * Debug-only flag that, when enabled, makes self-hosted environments behave as cloud + * environments for premium-upgrade gating. Used by QA to test the premium upgrade flow + * against internal self-hosted environments. + */ + data object DebugDisableSelfHostPremiumCheck : FlagKey() { + override val keyName: String = "debug-disable-self-host-premium-check" + override val defaultValue: Boolean = false + } + //region Dummy keys for testing /** * Data object holding the key for a [Boolean] flag to be used in tests. diff --git a/ui/src/main/kotlin/com/bitwarden/ui/platform/components/debug/FeatureFlagListItems.kt b/ui/src/main/kotlin/com/bitwarden/ui/platform/components/debug/FeatureFlagListItems.kt index 003f2ae9e8..1e536f477f 100644 --- a/ui/src/main/kotlin/com/bitwarden/ui/platform/components/debug/FeatureFlagListItems.kt +++ b/ui/src/main/kotlin/com/bitwarden/ui/platform/components/debug/FeatureFlagListItems.kt @@ -39,6 +39,7 @@ fun FlagKey.ListItemContent( FlagKey.V2EncryptionPassword, FlagKey.V2EncryptionTde, FlagKey.NewItemTypes, + FlagKey.DebugDisableSelfHostPremiumCheck, -> { @Suppress("UNCHECKED_CAST") BooleanFlagItem( @@ -99,4 +100,7 @@ private fun FlagKey.getDisplayLabel(): String = when (this) { FlagKey.V2EncryptionPassword -> stringResource(BitwardenString.v2_encryption_password) FlagKey.V2EncryptionTde -> stringResource(BitwardenString.v2_encryption_tde) FlagKey.NewItemTypes -> stringResource(BitwardenString.new_item_types) + FlagKey.DebugDisableSelfHostPremiumCheck -> { + stringResource(BitwardenString.debug_disable_self_host_premium_check) + } } diff --git a/ui/src/main/res/values/strings_non_localized.xml b/ui/src/main/res/values/strings_non_localized.xml index 61a386ec9d..dabe4891c8 100644 --- a/ui/src/main/res/values/strings_non_localized.xml +++ b/ui/src/main/res/values/strings_non_localized.xml @@ -53,6 +53,7 @@ V2 Encryption - Password Manage devices New Item Types + Debug: Disable self-host premium check