PM-20152: Remove import logins flow feature flag (#5580)

This commit is contained in:
David Perez 2025-07-25 09:14:48 -05:00 committed by GitHub
parent 2f2ec71fc4
commit e665c386ff
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 87 additions and 391 deletions

View File

@ -7,7 +7,6 @@ import com.x8bit.bitwarden.data.autofill.manager.AutofillEnabledManager
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
import com.x8bit.bitwarden.data.platform.manager.model.CoachMarkTourType
import com.x8bit.bitwarden.data.platform.manager.model.FirstTimeState
import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
@ -31,7 +30,6 @@ class FirstTimeActionManagerImpl @Inject constructor(
private val authDiskSource: AuthDiskSource,
private val settingsDiskSource: SettingsDiskSource,
private val vaultDiskSource: VaultDiskSource,
private val featureFlagManager: FeatureFlagManager,
private val autofillEnabledManager: AutofillEnabledManager,
) : FirstTimeActionManager {
@ -101,16 +99,9 @@ class FirstTimeActionManagerImpl @Inject constructor(
.activeUserIdChangesFlow
.filterNotNull()
.flatMapLatest {
combine(
getShowImportLoginsSettingBadgeFlowInternal(userId = it),
featureFlagManager.getFeatureFlagFlow(FlagKey.ImportLoginsFlow),
) { showImportLogins, importLoginsEnabled ->
val shouldShowImportLoginsSettings = showImportLogins && importLoginsEnabled
listOf(shouldShowImportLoginsSettings)
}
.map { list ->
list.count { showImportLogins -> showImportLogins }
}
getShowImportLoginsSettingBadgeFlowInternal(userId = it)
.map { showImportLogins -> listOf(showImportLogins) }
.map { list -> list.count { showImportLogins -> showImportLogins } }
}
.stateIn(
scope = unconfinedScope,

View File

@ -353,14 +353,12 @@ object PlatformManagerModule {
settingsDiskSource: SettingsDiskSource,
vaultDiskSource: VaultDiskSource,
dispatcherManager: DispatcherManager,
featureFlagManager: FeatureFlagManager,
autofillEnabledManager: AutofillEnabledManager,
): FirstTimeActionManager = FirstTimeActionManagerImpl(
authDiskSource = authDiskSource,
settingsDiskSource = settingsDiskSource,
vaultDiskSource = vaultDiskSource,
dispatcherManager = dispatcherManager,
featureFlagManager = featureFlagManager,
autofillEnabledManager = autofillEnabledManager,
)

View File

@ -22,7 +22,6 @@ sealed class FlagKey<out T : Any> {
val activeFlags: List<FlagKey<*>> by lazy {
listOf(
EmailVerification,
ImportLoginsFlow,
CredentialExchangeProtocolImport,
CredentialExchangeProtocolExport,
SingleTapPasskeyCreation,
@ -42,14 +41,6 @@ sealed class FlagKey<out T : Any> {
override val defaultValue: Boolean = false
}
/**
* Data object holding the feature flag key for the import logins feature.
*/
data object ImportLoginsFlow : FlagKey<Boolean>() {
override val keyName: String = "import-logins-flow"
override val defaultValue: Boolean = false
}
/**
* Data object holding hte feature flag key for the Credential Exchange Protocol (CXP) import
* feature.

View File

@ -26,7 +26,6 @@ fun <T : Any> FlagKey<T>.ListItemContent(
}
FlagKey.EmailVerification,
FlagKey.ImportLoginsFlow,
FlagKey.CredentialExchangeProtocolImport,
FlagKey.CredentialExchangeProtocolExport,
FlagKey.CipherKeyEncryption,
@ -77,7 +76,6 @@ private fun <T : Any> FlagKey<T>.getDisplayLabel(): String = when (this) {
-> this.keyName
FlagKey.EmailVerification -> stringResource(BitwardenString.email_verification)
FlagKey.ImportLoginsFlow -> stringResource(BitwardenString.import_logins_flow)
FlagKey.CredentialExchangeProtocolImport -> stringResource(BitwardenString.cxp_import)
FlagKey.CredentialExchangeProtocolExport -> stringResource(BitwardenString.cxp_export)
FlagKey.CipherKeyEncryption -> stringResource(BitwardenString.cipher_key_encryption)

View File

@ -21,7 +21,6 @@ import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.core.net.toUri
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.bitwarden.ui.platform.base.util.EventsEffect
@ -34,13 +33,10 @@ import com.bitwarden.ui.platform.resource.BitwardenDrawable
import com.bitwarden.ui.platform.resource.BitwardenString
import com.x8bit.bitwarden.ui.platform.components.card.BitwardenActionCard
import com.x8bit.bitwarden.ui.platform.components.card.actionCardExitAnimation
import com.x8bit.bitwarden.ui.platform.components.row.BitwardenExternalLinkRow
import com.x8bit.bitwarden.ui.platform.components.row.BitwardenTextRow
import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
import com.x8bit.bitwarden.ui.platform.components.snackbar.BitwardenSnackbarHost
import com.x8bit.bitwarden.ui.platform.components.snackbar.rememberBitwardenSnackbarHostState
import com.x8bit.bitwarden.ui.platform.composition.LocalIntentManager
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
/**
* Displays the vault settings screen.
@ -54,7 +50,6 @@ fun VaultSettingsScreen(
onNavigateToFolders: () -> Unit,
onNavigateToImportLogins: () -> Unit,
viewModel: VaultSettingsViewModel = hiltViewModel(),
intentManager: IntentManager = LocalIntentManager.current,
) {
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
@ -64,14 +59,7 @@ fun VaultSettingsScreen(
VaultSettingsEvent.NavigateBack -> onNavigateBack()
VaultSettingsEvent.NavigateToExportVault -> onNavigateToExportVault()
VaultSettingsEvent.NavigateToFolders -> onNavigateToFolders()
is VaultSettingsEvent.NavigateToImportVault -> {
if (state.isNewImportLoginsFlowEnabled) {
onNavigateToImportLogins()
} else {
intentManager.launchUri(event.url.toUri())
}
}
is VaultSettingsEvent.NavigateToImportVault -> onNavigateToImportLogins()
is VaultSettingsEvent.ShowSnackbar -> snackbarHostState.showSnackbar(event.data)
}
}
@ -105,7 +93,7 @@ fun VaultSettingsScreen(
) {
Spacer(modifier = Modifier.height(12.dp))
AnimatedVisibility(
visible = state.shouldShowImportCard,
visible = state.showImportActionCard,
label = "ImportLoginsActionCard",
exit = actionCardExitAnimation(),
) {
@ -159,38 +147,18 @@ fun VaultSettingsScreen(
.fillMaxWidth(),
)
if (state.isNewImportLoginsFlowEnabled) {
BitwardenTextRow(
text = stringResource(BitwardenString.import_items),
onClick = remember(viewModel) {
{ viewModel.trySendAction(VaultSettingsAction.ImportItemsClick) }
},
withDivider = false,
cardStyle = CardStyle.Bottom,
modifier = Modifier
.testTag("ImportItemsLinkItemView")
.standardHorizontalMargin()
.fillMaxWidth(),
)
} else {
BitwardenExternalLinkRow(
text = stringResource(BitwardenString.import_items),
onConfirmClick = remember(viewModel) {
{ viewModel.trySendAction(VaultSettingsAction.ImportItemsClick) }
},
withDivider = false,
dialogTitle = stringResource(id = BitwardenString.continue_to_web_app),
dialogMessage = stringResource(
id = BitwardenString.you_can_import_data_to_your_vault_on_x,
state.importUrl,
),
cardStyle = CardStyle.Bottom,
modifier = Modifier
.testTag("ImportItemsLinkItemView")
.standardHorizontalMargin()
.fillMaxWidth(),
)
}
BitwardenTextRow(
text = stringResource(BitwardenString.import_items),
onClick = remember(viewModel) {
{ viewModel.trySendAction(VaultSettingsAction.ImportItemsClick) }
},
withDivider = false,
cardStyle = CardStyle.Bottom,
modifier = Modifier
.testTag("ImportItemsLinkItemView")
.standardHorizontalMargin()
.fillMaxWidth(),
)
Spacer(modifier = Modifier.height(height = 16.dp))
Spacer(modifier = Modifier.navigationBarsPadding())
}

View File

@ -1,13 +1,9 @@
package com.x8bit.bitwarden.ui.platform.feature.settings.vault
import androidx.lifecycle.viewModelScope
import com.bitwarden.data.repository.util.toBaseWebVaultImportUrl
import com.bitwarden.ui.platform.base.BackgroundEvent
import com.bitwarden.ui.platform.base.BaseViewModel
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManager
import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
import com.x8bit.bitwarden.ui.platform.components.snackbar.BitwardenSnackbarData
import com.x8bit.bitwarden.ui.platform.manager.snackbar.SnackbarRelay
import com.x8bit.bitwarden.ui.platform.manager.snackbar.SnackbarRelayManager
@ -24,31 +20,17 @@ import javax.inject.Inject
@Suppress("TooManyFunctions")
@HiltViewModel
class VaultSettingsViewModel @Inject constructor(
environmentRepository: EnvironmentRepository,
featureFlagManager: FeatureFlagManager,
snackbarRelayManager: SnackbarRelayManager,
private val firstTimeActionManager: FirstTimeActionManager,
) : BaseViewModel<VaultSettingsState, VaultSettingsEvent, VaultSettingsAction>(
initialState = run {
val firstTimeState = firstTimeActionManager.currentOrDefaultUserFirstTimeState
VaultSettingsState(
importUrl = environmentRepository
.environment
.environmentUrlData
.toBaseWebVaultImportUrl,
isNewImportLoginsFlowEnabled = featureFlagManager
.getFeatureFlag(FlagKey.ImportLoginsFlow),
showImportActionCard = firstTimeState.showImportLoginsCardInSettings,
)
},
) {
init {
featureFlagManager
.getFeatureFlagFlow(FlagKey.ImportLoginsFlow)
.map { VaultSettingsAction.Internal.ImportLoginsFeatureFlagChanged(it) }
.onEach(::sendAction)
.launchIn(viewModelScope)
firstTimeActionManager
.firstTimeStateFlow
.map {
@ -78,10 +60,6 @@ class VaultSettingsViewModel @Inject constructor(
private fun handleInternalAction(action: VaultSettingsAction.Internal) {
when (action) {
is VaultSettingsAction.Internal.ImportLoginsFeatureFlagChanged -> {
handleImportLoginsFeatureFlagChanged(action)
}
is VaultSettingsAction.Internal.UserFirstTimeStateChanged -> {
handleUserFirstTimeStateChanged(action)
}
@ -99,12 +77,12 @@ class VaultSettingsViewModel @Inject constructor(
}
private fun handleImportLoginsCardDismissClicked() {
if (!state.shouldShowImportCard) return
if (!state.showImportActionCard) return
firstTimeActionManager.storeShowImportLoginsSettingsBadge(showBadge = false)
}
private fun handleImportLoginsCardClicked() {
sendEvent(VaultSettingsEvent.NavigateToImportVault(state.importUrl))
sendEvent(VaultSettingsEvent.NavigateToImportVault)
}
private fun handleUserFirstTimeStateChanged(
@ -115,14 +93,6 @@ class VaultSettingsViewModel @Inject constructor(
}
}
private fun handleImportLoginsFeatureFlagChanged(
action: VaultSettingsAction.Internal.ImportLoginsFeatureFlagChanged,
) {
mutableStateFlow.update {
it.copy(isNewImportLoginsFlowEnabled = action.isEnabled)
}
}
private fun handleBackClicked() {
sendEvent(VaultSettingsEvent.NavigateBack)
}
@ -136,9 +106,7 @@ class VaultSettingsViewModel @Inject constructor(
}
private fun handleImportItemsClicked() {
sendEvent(
VaultSettingsEvent.NavigateToImportVault(state.importUrl),
)
sendEvent(VaultSettingsEvent.NavigateToImportVault)
}
}
@ -146,16 +114,8 @@ class VaultSettingsViewModel @Inject constructor(
* Models the state for the VaultSettingScreen.
*/
data class VaultSettingsState(
val importUrl: String,
val isNewImportLoginsFlowEnabled: Boolean,
private val showImportActionCard: Boolean,
) {
/**
* Should only show the import action card if the import logins feature flag is enabled.
*/
val shouldShowImportCard: Boolean
get() = showImportActionCard && isNewImportLoginsFlowEnabled
}
val showImportActionCard: Boolean,
)
/**
* Models events for the vault screen.
@ -169,7 +129,7 @@ sealed class VaultSettingsEvent {
/**
* Navigate to the import vault URL.
*/
data class NavigateToImportVault(val url: String) : VaultSettingsEvent()
data object NavigateToImportVault : VaultSettingsEvent()
/**
* Navigate to the Export Vault screen.
@ -225,14 +185,6 @@ sealed class VaultSettingsAction {
* Internal actions not performed by user interation
*/
sealed class Internal : VaultSettingsAction() {
/**
* Indicates that the import logins feature flag has changed.
*/
data class ImportLoginsFeatureFlagChanged(
val isEnabled: Boolean,
) : Internal()
/**
* Indicates user first time state has changed.
*/

View File

@ -164,14 +164,7 @@ class VaultViewModel @Inject constructor(
authRepository
.userStateFlow
.combine(
featureFlagManager.getFeatureFlagFlow(FlagKey.ImportLoginsFlow),
) { userState, importLoginsEnabled ->
VaultAction.Internal.UserStateUpdateReceive(
userState = userState,
importLoginsFlowEnabled = importLoginsEnabled,
)
}
.map { VaultAction.Internal.UserStateUpdateReceive(it) }
.onEach(::sendAction)
.launchIn(viewModelScope)
@ -805,8 +798,6 @@ class VaultViewModel @Inject constructor(
.any(),
)
val appBarTitle = vaultFilterData.toAppBarTitle()
val shouldShowImportActionCard = action.importLoginsFlowEnabled &&
firstTimeState.showImportLoginsCard
mutableStateFlow.update {
val accountSummaries = userState.toAccountSummaries()
@ -818,7 +809,7 @@ class VaultViewModel @Inject constructor(
accountSummaries = accountSummaries,
vaultFilterData = vaultFilterData,
isPremium = userState.activeAccount.isPremium,
showImportActionCard = shouldShowImportActionCard,
showImportActionCard = firstTimeState.showImportLoginsCard,
)
}
}
@ -1704,7 +1695,6 @@ sealed class VaultAction {
*/
data class UserStateUpdateReceive(
val userState: UserState?,
val importLoginsFlowEnabled: Boolean,
) : Internal()
/**

View File

@ -14,7 +14,6 @@ import com.x8bit.bitwarden.data.autofill.manager.AutofillEnabledManager
import com.x8bit.bitwarden.data.platform.datasource.disk.util.FakeSettingsDiskSource
import com.x8bit.bitwarden.data.platform.manager.model.CoachMarkTourType
import com.x8bit.bitwarden.data.platform.manager.model.FirstTimeState
import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource
import io.mockk.every
import io.mockk.mockk
@ -35,13 +34,6 @@ class FirstTimeActionManagerTest {
private val vaultDiskSource = mockk<VaultDiskSource> {
every { getCiphersFlow(any()) } returns mutableCiphersListFlow
}
private val mutableImportLoginsFlow = MutableStateFlow(false)
private val mutableOnboardingFeatureFlow = MutableStateFlow(false)
private val featureFlagManager = mockk<FeatureFlagManager> {
every { getFeatureFlagFlow(FlagKey.ImportLoginsFlow) } returns mutableImportLoginsFlow
}
private val mutableAutofillEnabledFlow = MutableStateFlow(false)
private val autofillEnabledManager = mockk<AutofillEnabledManager> {
every { isAutofillEnabledStateFlow } returns mutableAutofillEnabledFlow
@ -52,14 +44,13 @@ class FirstTimeActionManagerTest {
authDiskSource = fakeAuthDiskSource,
settingsDiskSource = fakeSettingsDiskSource,
vaultDiskSource = vaultDiskSource,
featureFlagManager = featureFlagManager,
dispatcherManager = FakeDispatcherManager(),
autofillEnabledManager = autofillEnabledManager,
)
@Suppress("MaxLineLength")
@Test
fun `allAutoFillSettingsBadgeCountFlow should emit the value of flags set to true and update when saved value is changed or autofill enabled state changes`() =
fun `allAutoFillSettingsBadgeCountFlow should update when saved value is changed or autofill enabled state changes`() =
runTest {
fakeAuthDiskSource.userState = MOCK_USER_STATE
firstTimeActionManager.allAutofillSettingsBadgeCountFlow.test {
@ -84,81 +75,66 @@ class FirstTimeActionManagerTest {
}
}
@Suppress("MaxLineLength")
@Test
fun `allSecuritySettingsBadgeCountFlow should emit the value of flags set to true and update when changed`() =
runTest {
fakeAuthDiskSource.userState = MOCK_USER_STATE
firstTimeActionManager.allSecuritySettingsBadgeCountFlow.test {
assertEquals(0, awaitItem())
fakeSettingsDiskSource.storeShowUnlockSettingBadge(
userId = USER_ID,
showBadge = true,
)
assertEquals(1, awaitItem())
fakeSettingsDiskSource.storeShowUnlockSettingBadge(
userId = USER_ID,
showBadge = false,
)
assertEquals(0, awaitItem())
}
fun `allSecuritySettingsBadgeCountFlow should update when changed`() = runTest {
fakeAuthDiskSource.userState = MOCK_USER_STATE
firstTimeActionManager.allSecuritySettingsBadgeCountFlow.test {
assertEquals(0, awaitItem())
fakeSettingsDiskSource.storeShowUnlockSettingBadge(
userId = USER_ID,
showBadge = true,
)
assertEquals(1, awaitItem())
fakeSettingsDiskSource.storeShowUnlockSettingBadge(
userId = USER_ID,
showBadge = false,
)
assertEquals(0, awaitItem())
}
}
@Suppress("MaxLineLength")
@Test
fun `allSettingsBadgeCountFlow should emit the value of all flags set to true and update when changed`() =
runTest {
fakeAuthDiskSource.userState = MOCK_USER_STATE
firstTimeActionManager.allSettingsBadgeCountFlow.test {
assertEquals(0, awaitItem())
fakeSettingsDiskSource.storeShowAutoFillSettingBadge(
userId = USER_ID,
showBadge = true,
)
assertEquals(1, awaitItem())
fakeSettingsDiskSource.storeShowUnlockSettingBadge(
userId = USER_ID,
showBadge = true,
)
assertEquals(2, awaitItem())
fakeSettingsDiskSource.storeShowAutoFillSettingBadge(
userId = USER_ID,
showBadge = false,
)
assertEquals(1, awaitItem())
// for the import logins count it is dependent on the feature flag state and
// cipher list being empty
mutableImportLoginsFlow.update { true }
fakeSettingsDiskSource.storeShowImportLoginsSettingBadge(USER_ID, true)
assertEquals(2, awaitItem())
}
fun `allSettingsBadgeCountFlow should update when changed`() = runTest {
fakeAuthDiskSource.userState = MOCK_USER_STATE
firstTimeActionManager.allSettingsBadgeCountFlow.test {
assertEquals(0, awaitItem())
fakeSettingsDiskSource.storeShowAutoFillSettingBadge(
userId = USER_ID,
showBadge = true,
)
assertEquals(1, awaitItem())
fakeSettingsDiskSource.storeShowUnlockSettingBadge(
userId = USER_ID,
showBadge = true,
)
assertEquals(2, awaitItem())
fakeSettingsDiskSource.storeShowAutoFillSettingBadge(
userId = USER_ID,
showBadge = false,
)
assertEquals(1, awaitItem())
// for the import logins count it is dependent on the cipher list being empty
fakeSettingsDiskSource.storeShowImportLoginsSettingBadge(USER_ID, true)
assertEquals(2, awaitItem())
}
}
@Suppress("MaxLineLength")
@Test
fun `allVaultSettingsBadgeCountFlow should emit the value of all flags set to true and update when dependent states are changed changed`() =
fun `allVaultSettingsBadgeCountFlow should update when dependent states are changed changed`() =
runTest {
fakeAuthDiskSource.userState = MOCK_USER_STATE
val mockCipher = mockk<SyncResponseJson.Cipher>(relaxed = true)
// For the import logins count to register, the feature flag for ImportLoginsFlow must
// be enabled, the cipher list must not be empty, and the value saved to disk should be
// true.
// For the import logins count to register, the cipher list must not be empty, and the
// value saved to disk should be true.
firstTimeActionManager.allVaultSettingsBadgeCountFlow.test {
assertEquals(0, awaitItem())
mutableImportLoginsFlow.update { true }
fakeSettingsDiskSource.storeShowImportLoginsSettingBadge(USER_ID, true)
assertEquals(1, awaitItem())
mutableImportLoginsFlow.update { false }
assertEquals(0, awaitItem())
mutableImportLoginsFlow.update { true }
assertEquals(1, awaitItem())
fakeSettingsDiskSource.storeShowImportLoginsSettingBadge(USER_ID, false)
assertEquals(0, awaitItem())
fakeSettingsDiskSource.storeShowImportLoginsSettingBadge(USER_ID, true)
assertEquals(1, awaitItem())
mutableCiphersListFlow.update {
listOf(mockCipher)
}
mutableCiphersListFlow.update { listOf(mockCipher) }
assertEquals(0, awaitItem())
}
}
@ -167,8 +143,7 @@ class FirstTimeActionManagerTest {
fun `firstTimeStateFlow should emit changes when items in the first time state change`() =
runTest {
firstTimeActionManager.firstTimeStateFlow.test {
fakeAuthDiskSource.userState =
MOCK_USER_STATE
fakeAuthDiskSource.userState = MOCK_USER_STATE
assertEquals(
FirstTimeState(
showImportLoginsCard = true,
@ -208,40 +183,35 @@ class FirstTimeActionManagerTest {
@Test
fun `storeShowAutoFillSettingBadge should store value of false to disk`() {
fakeAuthDiskSource.userState =
MOCK_USER_STATE
fakeAuthDiskSource.userState = MOCK_USER_STATE
firstTimeActionManager.storeShowAutoFillSettingBadge(showBadge = false)
assertFalse(fakeSettingsDiskSource.getShowAutoFillSettingBadge(userId = USER_ID)!!)
}
@Test
fun `storeShowAutoFillSettingBadge should store value of true to disk`() {
fakeAuthDiskSource.userState =
MOCK_USER_STATE
fakeAuthDiskSource.userState = MOCK_USER_STATE
firstTimeActionManager.storeShowAutoFillSettingBadge(showBadge = true)
assertTrue(fakeSettingsDiskSource.getShowAutoFillSettingBadge(userId = USER_ID)!!)
}
@Test
fun `getShowAutoFillSettingBadge should return the value saved to disk`() {
fakeAuthDiskSource.userState =
MOCK_USER_STATE
fakeAuthDiskSource.userState = MOCK_USER_STATE
firstTimeActionManager.storeShowAutoFillSettingBadge(showBadge = true)
assertTrue(fakeSettingsDiskSource.getShowAutoFillSettingBadge(userId = USER_ID)!!)
}
@Test
fun `storeShowUnlockSettingBadge should store value of false to disk`() {
fakeAuthDiskSource.userState =
MOCK_USER_STATE
fakeAuthDiskSource.userState = MOCK_USER_STATE
firstTimeActionManager.storeShowUnlockSettingBadge(showBadge = false)
assertFalse(fakeSettingsDiskSource.getShowUnlockSettingBadge(userId = USER_ID)!!)
}
@Test
fun `storeShowUnlockSettingBadge should store value of true to disk`() {
fakeAuthDiskSource.userState =
MOCK_USER_STATE
fakeAuthDiskSource.userState = MOCK_USER_STATE
firstTimeActionManager.storeShowUnlockSettingBadge(showBadge = true)
assertTrue(fakeSettingsDiskSource.getShowUnlockSettingBadge(userId = USER_ID)!!)
}
@ -303,8 +273,6 @@ class FirstTimeActionManagerTest {
@Test
fun `shouldShowAddLoginCoachMarkFlow updates when disk source updates`() = runTest {
fakeAuthDiskSource.userState = MOCK_USER_STATE
// Enable the feature for this test.
mutableOnboardingFeatureFlow.update { true }
firstTimeActionManager.shouldShowAddLoginCoachMarkFlow.test {
assertTrue(awaitItem())
fakeSettingsDiskSource.storeShouldShowAddLoginCoachMark(shouldShow = false)
@ -325,8 +293,6 @@ class FirstTimeActionManagerTest {
every { organizationId } returns null
}
fakeAuthDiskSource.userState = MOCK_USER_STATE
// Enable feature flag so flow emits updates from disk.
mutableOnboardingFeatureFlow.update { true }
mutableCiphersListFlow.update {
listOf(
mockJsonWithNoLogin,
@ -356,8 +322,6 @@ class FirstTimeActionManagerTest {
@Test
fun `shouldShowGeneratorCoachMarkFlow updates when disk source updates`() = runTest {
fakeAuthDiskSource.userState = MOCK_USER_STATE
// Enable feature flag so flow emits updates from disk.
mutableOnboardingFeatureFlow.update { true }
firstTimeActionManager.shouldShowGeneratorCoachMarkFlow.test {
assertTrue(awaitItem())
fakeSettingsDiskSource.storeShouldShowGeneratorCoachMark(shouldShow = false)
@ -378,8 +342,6 @@ class FirstTimeActionManagerTest {
every { organizationId } returns null
}
fakeAuthDiskSource.userState = MOCK_USER_STATE
// Enable feature flag so flow emits updates from disk.
mutableOnboardingFeatureFlow.update { true }
mutableCiphersListFlow.update {
listOf(
mockJsonWithNoLogin,
@ -406,8 +368,6 @@ class FirstTimeActionManagerTest {
every { organizationId } returns "1234"
}
fakeAuthDiskSource.userState = MOCK_USER_STATE
// Enable feature flag so flow emits updates from disk.
mutableOnboardingFeatureFlow.update { true }
mutableCiphersListFlow.update {
listOf(mockJsonWithLoginAndWithOrganizationId)
}

View File

@ -13,10 +13,6 @@ class FlagKeyTest {
FlagKey.EmailVerification.keyName,
"email-verification",
)
assertEquals(
FlagKey.ImportLoginsFlow.keyName,
"import-logins-flow",
)
assertEquals(
FlagKey.CredentialExchangeProtocolImport.keyName,
"cxp-import-mobile",
@ -56,7 +52,6 @@ class FlagKeyTest {
assertTrue(
listOf(
FlagKey.EmailVerification,
FlagKey.ImportLoginsFlow,
FlagKey.CredentialExchangeProtocolImport,
FlagKey.CredentialExchangeProtocolExport,
FlagKey.SingleTapPasskeyCreation,

View File

@ -145,7 +145,6 @@ class DebugMenuViewModelTest : BaseViewModelTest() {
private val DEFAULT_MAP_VALUE: ImmutableMap<FlagKey<Any>, Any> = persistentMapOf(
FlagKey.EmailVerification to true,
FlagKey.ImportLoginsFlow to true,
FlagKey.CredentialExchangeProtocolImport to true,
FlagKey.CredentialExchangeProtocolExport to true,
FlagKey.SingleTapPasskeyCreation to true,
@ -157,7 +156,6 @@ private val DEFAULT_MAP_VALUE: ImmutableMap<FlagKey<Any>, Any> = persistentMapOf
private val UPDATED_MAP_VALUE: ImmutableMap<FlagKey<Any>, Any> = persistentMapOf(
FlagKey.EmailVerification to false,
FlagKey.ImportLoginsFlow to false,
FlagKey.CredentialExchangeProtocolImport to false,
FlagKey.CredentialExchangeProtocolExport to false,
FlagKey.SingleTapPasskeyCreation to false,

View File

@ -1,20 +1,15 @@
package com.x8bit.bitwarden.ui.platform.feature.settings.vault
import androidx.compose.ui.test.assert
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertIsNotDisplayed
import androidx.compose.ui.test.hasAnyAncestor
import androidx.compose.ui.test.isDialog
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performScrollTo
import androidx.core.net.toUri
import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow
import com.bitwarden.ui.util.asText
import com.x8bit.bitwarden.ui.platform.base.BitwardenComposeTest
import com.x8bit.bitwarden.ui.platform.components.snackbar.BitwardenSnackbarData
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
@ -22,7 +17,6 @@ import io.mockk.runs
import io.mockk.verify
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
@ -36,14 +30,9 @@ class VaultSettingsScreenTest : BitwardenComposeTest() {
private val mutableEventFlow = bufferedMutableSharedFlow<VaultSettingsEvent>()
private val mutableStateFlow = MutableStateFlow(
VaultSettingsState(
importUrl = "testUrl/#/tools/import",
isNewImportLoginsFlowEnabled = false,
showImportActionCard = false,
),
)
private val intentManager: IntentManager = mockk(relaxed = true) {
every { launchUri(any()) } just runs
}
val viewModel = mockk<VaultSettingsViewModel>(relaxed = true) {
every { eventFlow } returns mutableEventFlow
@ -52,9 +41,7 @@ class VaultSettingsScreenTest : BitwardenComposeTest() {
@Before
fun setup() {
setContent(
intentManager = intentManager,
) {
setContent {
VaultSettingsScreen(
viewModel = viewModel,
onNavigateBack = { onNavigateBackCalled = true },
@ -80,42 +67,6 @@ class VaultSettingsScreenTest : BitwardenComposeTest() {
}
}
@Test
fun `import items click should display dialog and confirming should send ImportItemsClick`() {
composeTestRule.onNodeWithText("Import items").performClick()
composeTestRule
.onNodeWithText("Continue")
.assert(hasAnyAncestor(isDialog()))
.assertIsDisplayed()
.performClick()
verify {
viewModel.trySendAction(VaultSettingsAction.ImportItemsClick)
}
}
@Test
fun `import items click should display dialog & canceling should not send ImportItemsClick`() {
composeTestRule.onNodeWithText("Import items").performClick()
composeTestRule
.onNodeWithText("Cancel")
.assert(hasAnyAncestor(isDialog()))
.assertIsDisplayed()
.performClick()
verify(exactly = 0) {
viewModel.trySendAction(VaultSettingsAction.ImportItemsClick)
}
}
@Test
fun `import items click should display dialog with importUrl`() {
composeTestRule.onNodeWithText("Import items").performClick()
composeTestRule
.onNodeWithText(mutableStateFlow.value.importUrl, substring = true)
.assertIsDisplayed()
}
@Test
fun `NavigateBack should call onNavigateBack`() {
mutableEventFlow.tryEmit(VaultSettingsEvent.NavigateBack)
@ -135,43 +86,20 @@ class VaultSettingsScreenTest : BitwardenComposeTest() {
}
@Test
fun `on NavigateToImportVault should invoke IntentManager not lambda`() {
val testUrl = "testUrl"
mutableEventFlow.tryEmit(VaultSettingsEvent.NavigateToImportVault(testUrl))
verify {
intentManager.launchUri(testUrl.toUri())
}
assertFalse(onNavigateToImportLoginsCalled)
}
@Test
fun `when new logins feature flag is enabled send action right when import items is clicked`() {
mutableStateFlow.update {
it.copy(isNewImportLoginsFlowEnabled = true)
}
fun `send action right when import items is clicked`() {
composeTestRule.onNodeWithText("Import items").performClick()
verify { viewModel.trySendAction(VaultSettingsAction.ImportItemsClick) }
}
@Test
fun `when new logins feature flag is enabled NavigateToImportVault should invoke lambda`() {
mutableStateFlow.update {
it.copy(isNewImportLoginsFlowEnabled = true)
}
val testUrl = "testUrl"
mutableEventFlow.tryEmit(VaultSettingsEvent.NavigateToImportVault(testUrl))
fun `NavigateToImportVault should invoke lambda`() {
mutableEventFlow.tryEmit(VaultSettingsEvent.NavigateToImportVault)
assertTrue(onNavigateToImportLoginsCalled)
verify(exactly = 0) { intentManager.launchUri(testUrl.toUri()) }
}
@Test
fun `when new show action card is true the import logins card should show`() {
mutableStateFlow.update {
it.copy(
showImportActionCard = true,
isNewImportLoginsFlowEnabled = true,
)
}
mutableStateFlow.update { it.copy(showImportActionCard = true) }
composeTestRule
.onNodeWithText("Import saved logins")
.performScrollTo()
@ -186,12 +114,7 @@ class VaultSettingsScreenTest : BitwardenComposeTest() {
@Test
fun `when action card is visible clicking the close icon should send correct action`() {
mutableStateFlow.update {
it.copy(
showImportActionCard = true,
isNewImportLoginsFlowEnabled = true,
)
}
mutableStateFlow.update { it.copy(showImportActionCard = true) }
composeTestRule
.onNodeWithContentDescription("Close")
.performScrollTo()
@ -203,12 +126,7 @@ class VaultSettingsScreenTest : BitwardenComposeTest() {
@Test
fun `when action card is visible get started button should send correct action`() {
mutableStateFlow.update {
it.copy(
showImportActionCard = true,
isNewImportLoginsFlowEnabled = true,
)
}
mutableStateFlow.update { it.copy(showImportActionCard = true) }
composeTestRule
.onNodeWithText("Get started")
.performScrollTo()

View File

@ -4,11 +4,8 @@ import app.cash.turbine.test
import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow
import com.bitwarden.ui.platform.base.BaseViewModelTest
import com.bitwarden.ui.util.asText
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManager
import com.x8bit.bitwarden.data.platform.manager.model.FirstTimeState
import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
import com.x8bit.bitwarden.data.platform.repository.util.FakeEnvironmentRepository
import com.x8bit.bitwarden.ui.platform.components.snackbar.BitwardenSnackbarData
import com.x8bit.bitwarden.ui.platform.manager.snackbar.SnackbarRelay
import com.x8bit.bitwarden.ui.platform.manager.snackbar.SnackbarRelayManager
@ -26,12 +23,6 @@ import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
class VaultSettingsViewModelTest : BaseViewModelTest() {
private val environmentRepository = FakeEnvironmentRepository()
private val mutableImportLoginsFlagFlow = MutableStateFlow(false)
private val featureFlagManager = mockk<FeatureFlagManager> {
every { getFeatureFlagFlow(FlagKey.ImportLoginsFlow) } returns mutableImportLoginsFlagFlow
every { getFeatureFlag(FlagKey.ImportLoginsFlow) } returns false
}
private val mutableFirstTimeStateFlow = MutableStateFlow(DEFAULT_FIRST_TIME_STATE)
private val firstTimeActionManager = mockk<FirstTimeActionManager> {
every { currentOrDefaultUserFirstTimeState } returns DEFAULT_FIRST_TIME_STATE
@ -68,44 +59,25 @@ class VaultSettingsViewModelTest : BaseViewModelTest() {
}
@Test
fun `ImportItemsClick should emit send NavigateToImportVault with correct url`() = runTest {
fun `ImportItemsClick should emit send NavigateToImportVault`() = runTest {
val viewModel = createViewModel()
val expected = "https://vault.bitwarden.com/#/tools/import"
viewModel.eventFlow.test {
viewModel.trySendAction(VaultSettingsAction.ImportItemsClick)
assertEquals(
VaultSettingsEvent.NavigateToImportVault(expected),
VaultSettingsEvent.NavigateToImportVault,
awaitItem(),
)
}
}
@Test
fun `ImportLoginsFeatureFlagChanged should update state`() {
val viewModel = createViewModel()
assertFalse(
viewModel.stateFlow.value.isNewImportLoginsFlowEnabled,
)
mutableImportLoginsFlagFlow.update { true }
assertTrue(viewModel.stateFlow.value.isNewImportLoginsFlowEnabled)
}
@Test
fun `shouldShowImportCard should update when first time state changes`() = runTest {
mutableImportLoginsFlagFlow.update { true }
val viewModel = createViewModel()
assertTrue(viewModel.stateFlow.value.shouldShowImportCard)
assertTrue(viewModel.stateFlow.value.showImportActionCard)
mutableFirstTimeStateFlow.update {
it.copy(showImportLoginsCardInSettings = false)
}
assertFalse(viewModel.stateFlow.value.shouldShowImportCard)
}
@Test
fun `shouldShowImportCard should be false when feature flag not enabled`() = runTest {
val viewModel = createViewModel()
mutableImportLoginsFlagFlow.update { false }
assertFalse(viewModel.stateFlow.value.shouldShowImportCard)
assertFalse(viewModel.stateFlow.value.showImportActionCard)
}
@Suppress("MaxLineLength")
@ -113,12 +85,10 @@ class VaultSettingsViewModelTest : BaseViewModelTest() {
fun `ImportLoginsCardCtaClick action should set repository value to false and send navigation event`() =
runTest {
val viewModel = createViewModel()
val expected = "https://vault.bitwarden.com/#/tools/import"
mutableImportLoginsFlagFlow.update { true }
viewModel.eventFlow.test {
viewModel.trySendAction(VaultSettingsAction.ImportLoginsCardCtaClick)
assertEquals(
VaultSettingsEvent.NavigateToImportVault(url = expected),
VaultSettingsEvent.NavigateToImportVault,
awaitItem(),
)
}
@ -129,7 +99,6 @@ class VaultSettingsViewModelTest : BaseViewModelTest() {
@Test
fun `ImportLoginsCardDismissClick action should set repository value to false `() = runTest {
mutableImportLoginsFlagFlow.update { true }
val viewModel = createViewModel()
viewModel.trySendAction(VaultSettingsAction.ImportLoginsCardDismissClick)
verify(exactly = 1) {
@ -141,6 +110,7 @@ class VaultSettingsViewModelTest : BaseViewModelTest() {
@Test
fun `ImportLoginsCardDismissClick action should not set repository value to false if already false`() =
runTest {
mutableFirstTimeStateFlow.update { it.copy(showImportLoginsCardInSettings = false) }
val viewModel = createViewModel()
viewModel.trySendAction(VaultSettingsAction.ImportLoginsCardDismissClick)
verify(exactly = 0) {
@ -159,8 +129,6 @@ class VaultSettingsViewModelTest : BaseViewModelTest() {
}
private fun createViewModel(): VaultSettingsViewModel = VaultSettingsViewModel(
environmentRepository = environmentRepository,
featureFlagManager = featureFlagManager,
firstTimeActionManager = firstTimeActionManager,
snackbarRelayManager = snackbarRelayManager,
)

View File

@ -73,7 +73,6 @@ import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.test.advanceTimeBy
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.AfterEach
@ -172,13 +171,9 @@ class VaultViewModelTest : BaseViewModelTest() {
every { trackEvent(event = any()) } just runs
}
private val mutableImportLoginsFeatureFlow = MutableStateFlow(true)
private val mutableRemoveCardPolicyFeatureFlow = MutableStateFlow(false)
private val mutableSshKeyVaultItemsEnabledFlow = MutableStateFlow(false)
private val featureFlagManager: FeatureFlagManager = mockk {
every {
getFeatureFlagFlow(FlagKey.ImportLoginsFlow)
} returns mutableImportLoginsFeatureFlow
every {
getFeatureFlagFlow(FlagKey.RemoveCardPolicy)
} returns mutableRemoveCardPolicyFeatureFlow
@ -2433,30 +2428,6 @@ class VaultViewModelTest : BaseViewModelTest() {
}
}
@Suppress("MaxLineLength")
@Test
fun `when feature flag ImportLoginsFlow is disabled, should show action card should always be false`() =
runTest {
mutableImportLoginsFeatureFlow.update { false }
val viewModel = createViewModel()
viewModel.stateFlow.test {
assertEquals(
DEFAULT_STATE.copy(showImportActionCard = false),
awaitItem(),
)
mutableUserStateFlow.value = DEFAULT_USER_STATE.copy(
accounts = DEFAULT_USER_STATE.accounts.map {
it.copy(
firstTimeState = DEFAULT_FIRST_TIME_STATE.copy(
showImportLoginsCard = true,
),
)
},
)
expectNoEvents()
}
}
@Suppress("MaxLineLength")
@Test
fun `when DismissImportActionCard is sent, repository called to showImportLogins to false and storeShowImportLoginsBadge to true`() {

View File

@ -672,7 +672,6 @@ Do you want to switch to this account?</string>
<string name="continue_to_device_settings">Continue to device Settings?</string>
<string name="two_step_login_description_long">Make your account more secure by setting up two-step login in the Bitwarden web app.</string>
<string name="change_master_password_description_long">You can change your master password on the Bitwarden web app.</string>
<string name="you_can_import_data_to_your_vault_on_x">You can import data to your vault on %1$s.</string>
<string name="learn_more_about_how_to_use_bitwarden_on_the_help_center">Learn more about how to use Bitwarden on the Help center.</string>
<string name="privacy_policy_description_long">Check out our privacy policy on bitwarden.com.</string>
<string name="explore_more_features_of_your_bitwarden_account_on_the_web_app">Explore more features of your Bitwarden account on the web app.</string>

View File

@ -9,7 +9,6 @@
<!-- region Debug Menu -->
<string name="email_verification">Email Verification</string>
<string name="import_logins_flow">Import Logins Flow</string>
<string name="feature_flags">Feature Flags:</string>
<string name="debug_menu">Debug Menu</string>
<string name="reset_values">Reset values</string>