1
0
Fork 0
mirror of https://github.com/bitwarden/android.git synced 2025-02-22 16:49:13 +03:00

move feature check to manager

This commit is contained in:
Dave Severns 2025-01-22 09:35:49 -05:00
parent a95fe46e73
commit 45062df6f2
5 changed files with 32 additions and 46 deletions
app/src
main/java/com/x8bit/bitwarden
test/java/com/x8bit/bitwarden
data/platform/manager
ui/vault/feature/addedit

View file

@ -159,6 +159,13 @@ class FirstTimeActionManagerImpl @Inject constructor(
.getHasSeenAddLoginCoachMarkFlow() .getHasSeenAddLoginCoachMarkFlow()
// default value of false. // default value of false.
.map { it ?: false } .map { it ?: false }
.combine(
featureFlagManager.getFeatureFlagFlow(FlagKey.OnboardingFlow),
) { hasSeenTour, featureIsEnabled ->
// If the feature flag is off always return true so observers know
// the card has not been shown.
hasSeenTour || !featureIsEnabled
}
.distinctUntilChanged() .distinctUntilChanged()
/** /**

View file

@ -92,7 +92,9 @@ fun CoachMarkScope<AddEditItemCoachMark>.VaultAddEditContent(
actionText = stringResource(R.string.get_started), actionText = stringResource(R.string.get_started),
onActionClick = loginItemTypeHandlers.onStartLoginCoachMarkTour, onActionClick = loginItemTypeHandlers.onStartLoginCoachMarkTour,
onDismissClick = loginItemTypeHandlers.onDismissLearnAboutLoginsCard, onDismissClick = loginItemTypeHandlers.onDismissLearnAboutLoginsCard,
modifier = Modifier.standardHorizontalMargin(), modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
) )
} }
} }

View file

@ -15,13 +15,11 @@ import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CreateCredentialReques
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2RegisterCredentialResult import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2RegisterCredentialResult
import com.x8bit.bitwarden.data.autofill.fido2.model.UserVerificationRequirement import com.x8bit.bitwarden.data.autofill.fido2.model.UserVerificationRequirement
import com.x8bit.bitwarden.data.autofill.util.isActiveWithFido2Credentials import com.x8bit.bitwarden.data.autofill.util.isActiveWithFido2Credentials
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManager import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManager
import com.x8bit.bitwarden.data.platform.manager.PolicyManager import com.x8bit.bitwarden.data.platform.manager.PolicyManager
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager
import com.x8bit.bitwarden.data.platform.manager.event.OrganizationEventManager import com.x8bit.bitwarden.data.platform.manager.event.OrganizationEventManager
import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
import com.x8bit.bitwarden.data.platform.manager.model.OrganizationEvent import com.x8bit.bitwarden.data.platform.manager.model.OrganizationEvent
import com.x8bit.bitwarden.data.platform.manager.network.NetworkConnectionManager import com.x8bit.bitwarden.data.platform.manager.network.NetworkConnectionManager
import com.x8bit.bitwarden.data.platform.manager.util.toAutofillSaveItemOrNull import com.x8bit.bitwarden.data.platform.manager.util.toAutofillSaveItemOrNull
@ -66,7 +64,6 @@ import com.x8bit.bitwarden.ui.vault.model.VaultCollection
import com.x8bit.bitwarden.ui.vault.model.VaultIdentityTitle import com.x8bit.bitwarden.ui.vault.model.VaultIdentityTitle
import com.x8bit.bitwarden.ui.vault.model.VaultLinkedFieldType import com.x8bit.bitwarden.ui.vault.model.VaultLinkedFieldType
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
@ -108,7 +105,6 @@ class VaultAddEditViewModel @Inject constructor(
private val organizationEventManager: OrganizationEventManager, private val organizationEventManager: OrganizationEventManager,
private val networkConnectionManager: NetworkConnectionManager, private val networkConnectionManager: NetworkConnectionManager,
private val firstTimeActionManager: FirstTimeActionManager, private val firstTimeActionManager: FirstTimeActionManager,
private val featureFlagManager: FeatureFlagManager,
) : BaseViewModel<VaultAddEditState, VaultAddEditEvent, VaultAddEditAction>( ) : BaseViewModel<VaultAddEditState, VaultAddEditEvent, VaultAddEditAction>(
// We load the state from the savedStateHandle for testing purposes. // We load the state from the savedStateHandle for testing purposes.
initialState = savedStateHandle[KEY_STATE] initialState = savedStateHandle[KEY_STATE]
@ -221,13 +217,9 @@ class VaultAddEditViewModel @Inject constructor(
firstTimeActionManager firstTimeActionManager
.hasSeenAddLoginCoachMarkFlow .hasSeenAddLoginCoachMarkFlow
.combine( .map { hasSeenTour ->
featureFlagManager.getFeatureFlagFlow(key = FlagKey.OnboardingFlow),
) { hasSeenTour, onboardFeatureFlag ->
// The result of this will hide the card if the tour has been shown or if the
// onboarding flow feature is off.
VaultAddEditAction.Internal.HasSeenAddLoginCoachMarkValueChangeReceive( VaultAddEditAction.Internal.HasSeenAddLoginCoachMarkValueChangeReceive(
newValue = hasSeenTour || !onboardFeatureFlag, hasSeenCoachMark = hasSeenTour,
) )
} }
.onEach(::sendAction) .onEach(::sendAction)
@ -1495,7 +1487,7 @@ class VaultAddEditViewModel @Inject constructor(
) { ) {
mutableStateFlow.update { mutableStateFlow.update {
it.copy( it.copy(
hasSeenLearnAboutLoginsCard = action.newValue, hasSeenLearnAboutLoginsCard = action.hasSeenCoachMark,
) )
} }
} }
@ -3211,7 +3203,7 @@ sealed class VaultAddEditAction {
* The value for the hasSeenAddLoginCoachMark has changed. * The value for the hasSeenAddLoginCoachMark has changed.
*/ */
data class HasSeenAddLoginCoachMarkValueChangeReceive( data class HasSeenAddLoginCoachMarkValueChangeReceive(
val newValue: Boolean, val hasSeenCoachMark: Boolean,
) : Internal() ) : Internal()
} }
} }

View file

@ -36,8 +36,10 @@ class FirstTimeActionManagerTest {
} }
private val mutableImportLoginsFlow = MutableStateFlow(false) private val mutableImportLoginsFlow = MutableStateFlow(false)
private val mutableOnboardingFeatureFlow = MutableStateFlow(false)
private val featureFlagManager = mockk<FeatureFlagManager> { private val featureFlagManager = mockk<FeatureFlagManager> {
every { getFeatureFlagFlow(FlagKey.ImportLoginsFlow) } returns mutableImportLoginsFlow every { getFeatureFlagFlow(FlagKey.ImportLoginsFlow) } returns mutableImportLoginsFlow
every { getFeatureFlagFlow(FlagKey.OnboardingFlow) } returns mutableOnboardingFeatureFlow
} }
private val mutableAutofillEnabledFlow = MutableStateFlow(false) private val mutableAutofillEnabledFlow = MutableStateFlow(false)
@ -300,14 +302,28 @@ class FirstTimeActionManagerTest {
@Test @Test
fun `hasSeenAddLoginCoachMarkFlow updates when disk source updates`() = runTest { fun `hasSeenAddLoginCoachMarkFlow updates when disk source updates`() = runTest {
// Enable the feature for this test.
mutableOnboardingFeatureFlow.update { true }
firstTimeActionManager.hasSeenAddLoginCoachMarkFlow.test { firstTimeActionManager.hasSeenAddLoginCoachMarkFlow.test {
// null will be mapped to false // Null will be mapped to false.
assertFalse(awaitItem()) assertFalse(awaitItem())
fakeSettingsDiskSource.storeHasSeenAddLoginCoachMark(hasSeen = true) fakeSettingsDiskSource.storeHasSeenAddLoginCoachMark(hasSeen = true)
assertTrue(awaitItem()) assertTrue(awaitItem())
} }
} }
@Test
fun `hasSeenAddLoginCoachMarkFlow updates when feature flag for onboarding updates`() =
runTest {
firstTimeActionManager.hasSeenAddLoginCoachMarkFlow.test {
// Null will be mapped to false but feature being "off" will override to true.
assertTrue(awaitItem())
mutableOnboardingFeatureFlow.update { true }
// Will use the value from disk source (null ?: false).
assertFalse(awaitItem())
}
}
@Test @Test
fun `hasSeenAddLoginCoachMarkTour sets the value to true in the disk source`() { fun `hasSeenAddLoginCoachMarkTour sets the value to true in the disk source`() {
assertNull(fakeSettingsDiskSource.getHasSeenAddLoginCoachMark()) assertNull(fakeSettingsDiskSource.getHasSeenAddLoginCoachMark())

View file

@ -25,7 +25,6 @@ import com.x8bit.bitwarden.data.autofill.fido2.model.createMockFido2CreateCreden
import com.x8bit.bitwarden.data.autofill.model.AutofillSaveItem import com.x8bit.bitwarden.data.autofill.model.AutofillSaveItem
import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData
import com.x8bit.bitwarden.data.platform.base.FakeDispatcherManager import com.x8bit.bitwarden.data.platform.base.FakeDispatcherManager
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManager import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManager
import com.x8bit.bitwarden.data.platform.manager.PolicyManager import com.x8bit.bitwarden.data.platform.manager.PolicyManager
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager
@ -33,7 +32,6 @@ import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManagerImpl
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager
import com.x8bit.bitwarden.data.platform.manager.event.OrganizationEventManager import com.x8bit.bitwarden.data.platform.manager.event.OrganizationEventManager
import com.x8bit.bitwarden.data.platform.manager.model.FirstTimeState import com.x8bit.bitwarden.data.platform.manager.model.FirstTimeState
import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
import com.x8bit.bitwarden.data.platform.manager.model.OrganizationEvent import com.x8bit.bitwarden.data.platform.manager.model.OrganizationEvent
import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance
import com.x8bit.bitwarden.data.platform.manager.network.NetworkConnectionManager import com.x8bit.bitwarden.data.platform.manager.network.NetworkConnectionManager
@ -175,14 +173,6 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
every { hasSeenAddLoginCoachMarkFlow } returns mutableHasSeenAddLoginCoachMarkFlow every { hasSeenAddLoginCoachMarkFlow } returns mutableHasSeenAddLoginCoachMarkFlow
} }
// Feature flag default to be enabled.
private val mutableOnboardingFeatureFlagFlow = MutableStateFlow(true)
private val featureFlagManager = mockk<FeatureFlagManager> {
every {
getFeatureFlagFlow(FlagKey.OnboardingFlow)
} returns mutableOnboardingFeatureFlagFlow
}
@BeforeEach @BeforeEach
fun setup() { fun setup() {
mockkStatic(CipherView::toViewState) mockkStatic(CipherView::toViewState)
@ -2623,25 +2613,6 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
} }
} }
@Test
fun `when OnboardFlow feature flag is off, shouldShowLearnAboutNewLogins should be false`() {
mutableOnboardingFeatureFlagFlow.update { true }
val viewModel = createAddVaultItemViewModel(
savedStateHandle = createSavedStateHandleWithState(
state = createVaultAddItemState(
typeContentViewState = createLoginTypeContentViewState(),
),
vaultAddEditType = VaultAddEditType.AddItem(
vaultItemCipherType = VaultItemCipherType.LOGIN,
),
),
)
assertTrue(viewModel.stateFlow.value.shouldShowLearnAboutNewLogins)
mutableOnboardingFeatureFlagFlow.update { false }
assertFalse(viewModel.stateFlow.value.shouldShowLearnAboutNewLogins)
}
@Suppress("MaxLineLength") @Suppress("MaxLineLength")
@Test @Test
fun `when first time action manager has seen logins tour value updates to true shouldShowLearnAboutNewLogins should update to false`() { fun `when first time action manager has seen logins tour value updates to true shouldShowLearnAboutNewLogins should update to false`() {
@ -2710,7 +2681,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
@Suppress("MaxLineLength") @Suppress("MaxLineLength")
@Test @Test
fun `LearnAboutLoginsDismissed action calls first time action manager hasSeenAddLoginCoachMarkTour called and show coach mark event sent`() = fun `StartLearnAboutLogins action calls first time action manager hasSeenAddLoginCoachMarkTour called and show coach mark event sent`() =
runTest { runTest {
val viewModel = createAddVaultItemViewModel() val viewModel = createAddVaultItemViewModel()
viewModel.eventFlow.test { viewModel.eventFlow.test {
@ -3246,7 +3217,6 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
organizationEventManager = organizationEventManager, organizationEventManager = organizationEventManager,
networkConnectionManager = networkConnectionManager, networkConnectionManager = networkConnectionManager,
firstTimeActionManager = firstTimeActionManager, firstTimeActionManager = firstTimeActionManager,
featureFlagManager = featureFlagManager,
) )
} }
@ -4508,7 +4478,6 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
organizationEventManager = organizationEventManager, organizationEventManager = organizationEventManager,
networkConnectionManager = networkConnectionManager, networkConnectionManager = networkConnectionManager,
firstTimeActionManager = firstTimeActionManager, firstTimeActionManager = firstTimeActionManager,
featureFlagManager = featureFlagManager,
) )
private fun createVaultData( private fun createVaultData(