PM-23693: Remove Authenticator Sync flag from Authenticator app (#5515)

This commit is contained in:
David Perez
2025-07-11 08:27:10 -05:00
committed by GitHub
parent 0feac46711
commit d2f7d52132
17 changed files with 25 additions and 169 deletions

View File

@@ -16,11 +16,9 @@ import com.bitwarden.authenticator.data.authenticator.repository.model.SharedVer
import com.bitwarden.authenticator.data.authenticator.repository.model.TotpCodeResult
import com.bitwarden.authenticator.data.authenticator.repository.util.sortAlphabetically
import com.bitwarden.authenticator.data.authenticator.repository.util.toAuthenticatorItems
import com.bitwarden.authenticator.data.platform.manager.FeatureFlagManager
import com.bitwarden.authenticator.data.platform.manager.imports.ImportManager
import com.bitwarden.authenticator.data.platform.manager.imports.model.ImportDataResult
import com.bitwarden.authenticator.data.platform.manager.imports.model.ImportFileFormat
import com.bitwarden.authenticator.data.platform.manager.model.FlagKey
import com.bitwarden.authenticator.data.platform.repository.SettingsRepository
import com.bitwarden.authenticator.ui.platform.feature.settings.export.model.ExportVaultFormat
import com.bitwarden.authenticator.ui.platform.manager.intent.IntentManager
@@ -64,7 +62,6 @@ private const val STOP_TIMEOUT_DELAY_MS: Long = 5_000L
class AuthenticatorRepositoryImpl @Inject constructor(
private val authenticatorBridgeManager: AuthenticatorBridgeManager,
private val authenticatorDiskSource: AuthenticatorDiskSource,
private val featureFlagManager: FeatureFlagManager,
private val totpCodeManager: TotpCodeManager,
private val fileManager: FileManager,
private val importManager: ImportManager,
@@ -155,17 +152,9 @@ class AuthenticatorRepositoryImpl @Inject constructor(
@OptIn(ExperimentalCoroutinesApi::class)
override val sharedCodesStateFlow: StateFlow<SharedVerificationCodesState> by lazy {
featureFlagManager
.getFeatureFlagFlow(FlagKey.PasswordManagerSync)
.flatMapLatest { isFeatureEnabled ->
if (isFeatureEnabled) {
authenticatorBridgeManager
.accountSyncStateFlow
.flatMapLatest { it.toSharedVerificationCodesStateFlow() }
} else {
flowOf(SharedVerificationCodesState.FeatureNotEnabled)
}
}
authenticatorBridgeManager
.accountSyncStateFlow
.flatMapLatest { it.toSharedVerificationCodesStateFlow() }
.stateIn(
scope = unconfinedScope,
started = SharingStarted.WhileSubscribed(STOP_TIMEOUT_DELAY_MS),

View File

@@ -4,19 +4,14 @@ import android.content.Context
import com.bitwarden.authenticator.BuildConfig
import com.bitwarden.authenticator.data.auth.datasource.disk.AuthDiskSource
import com.bitwarden.authenticator.data.authenticator.repository.util.SymmetricKeyStorageProviderImpl
import com.bitwarden.authenticator.data.platform.manager.FeatureFlagManager
import com.bitwarden.authenticator.data.platform.manager.model.FlagKey
import com.bitwarden.authenticatorbridge.factory.AuthenticatorBridgeFactory
import com.bitwarden.authenticatorbridge.manager.AuthenticatorBridgeManager
import com.bitwarden.authenticatorbridge.manager.model.AccountSyncState
import com.bitwarden.authenticatorbridge.provider.SymmetricKeyStorageProvider
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import javax.inject.Singleton
/**
@@ -38,23 +33,11 @@ object AuthenticatorBridgeModule {
fun provideAuthenticatorBridgeManager(
factory: AuthenticatorBridgeFactory,
symmetricKeyStorageProvider: SymmetricKeyStorageProvider,
featureFlagManager: FeatureFlagManager,
): AuthenticatorBridgeManager =
if (featureFlagManager.getFeatureFlag(FlagKey.PasswordManagerSync)) {
factory.getAuthenticatorBridgeManager(
connectionType = BuildConfig.AUTHENTICATOR_BRIDGE_CONNECTION_TYPE,
symmetricKeyStorageProvider = symmetricKeyStorageProvider,
)
} else {
// If feature flag is not enabled, return no-op bridge manager so we never
// connect to bridge service:
object : AuthenticatorBridgeManager {
override val accountSyncStateFlow: StateFlow<AccountSyncState>
get() = MutableStateFlow(AccountSyncState.Loading)
override fun startAddTotpLoginItemFlow(totpUri: String): Boolean = false
}
}
factory.getAuthenticatorBridgeManager(
connectionType = BuildConfig.AUTHENTICATOR_BRIDGE_CONNECTION_TYPE,
symmetricKeyStorageProvider = symmetricKeyStorageProvider,
)
@Provides
fun providesSymmetricKeyStorageProvider(

View File

@@ -5,7 +5,6 @@ import com.bitwarden.authenticator.data.authenticator.manager.FileManager
import com.bitwarden.authenticator.data.authenticator.manager.TotpCodeManager
import com.bitwarden.authenticator.data.authenticator.repository.AuthenticatorRepository
import com.bitwarden.authenticator.data.authenticator.repository.AuthenticatorRepositoryImpl
import com.bitwarden.authenticator.data.platform.manager.FeatureFlagManager
import com.bitwarden.authenticator.data.platform.manager.imports.ImportManager
import com.bitwarden.authenticator.data.platform.repository.SettingsRepository
import com.bitwarden.authenticatorbridge.manager.AuthenticatorBridgeManager
@@ -28,7 +27,6 @@ object AuthenticatorRepositoryModule {
fun provideAuthenticatorRepository(
authenticatorBridgeManager: AuthenticatorBridgeManager,
authenticatorDiskSource: AuthenticatorDiskSource,
featureFlagManager: FeatureFlagManager,
dispatcherManager: DispatcherManager,
fileManager: FileManager,
importManager: ImportManager,
@@ -37,7 +35,6 @@ object AuthenticatorRepositoryModule {
): AuthenticatorRepository = AuthenticatorRepositoryImpl(
authenticatorBridgeManager = authenticatorBridgeManager,
authenticatorDiskSource = authenticatorDiskSource,
featureFlagManager = featureFlagManager,
dispatcherManager = dispatcherManager,
fileManager = fileManager,
importManager = importManager,

View File

@@ -22,7 +22,6 @@ sealed class FlagKey<out T : Any> {
val activeFlags: List<FlagKey<*>> by lazy {
listOf(
BitwardenAuthenticationEnabled,
PasswordManagerSync,
)
}
}
@@ -35,14 +34,6 @@ sealed class FlagKey<out T : Any> {
override val defaultValue: Boolean = false
}
/**
* Indicates whether syncing with the main Bitwarden password manager app should be enabled..
*/
data object PasswordManagerSync : FlagKey<Boolean>() {
override val keyName: String = "enable-pm-bwa-sync"
override val defaultValue: Boolean = false
}
/**
* Data object holding the key for a [Boolean] flag to be used in tests.
*/

View File

@@ -147,7 +147,6 @@ private fun FeatureFlagContent_preview() {
FeatureFlagContent(
featureFlagMap = mapOf(
FlagKey.BitwardenAuthenticationEnabled to true,
FlagKey.PasswordManagerSync to false,
),
onValueChange = { _, _ -> },
onResetValues = { },

View File

@@ -23,7 +23,6 @@ fun <T : Any> FlagKey<T>.ListItemContent(
-> Unit
FlagKey.BitwardenAuthenticationEnabled,
FlagKey.PasswordManagerSync,
-> BooleanFlagItem(
label = flagKey.getDisplayLabel(),
key = flagKey as FlagKey<Boolean>,
@@ -63,6 +62,4 @@ private fun <T : Any> FlagKey<T>.getDisplayLabel(): String = when (this) {
FlagKey.BitwardenAuthenticationEnabled ->
stringResource(R.string.bitwarden_authentication_enabled)
FlagKey.PasswordManagerSync -> stringResource(R.string.password_manager_sync)
}

View File

@@ -351,7 +351,7 @@ private fun VaultSettings(
BitwardenTextRow(
text = stringResource(id = R.string.sync_with_bitwarden_app),
description = annotatedStringResource(
id = R.string.this_feature_is_not_not_yet_available_for_self_hosted_users,
id = R.string.learn_more_link,
style = spanStyleOf(
color = MaterialTheme.colorScheme.onSurfaceVariant,
textStyle = MaterialTheme.typography.bodyMedium,

View File

@@ -10,9 +10,7 @@ import com.bitwarden.authenticator.R
import com.bitwarden.authenticator.data.authenticator.repository.AuthenticatorRepository
import com.bitwarden.authenticator.data.authenticator.repository.model.SharedVerificationCodesState
import com.bitwarden.authenticator.data.authenticator.repository.util.isSyncWithBitwardenEnabled
import com.bitwarden.authenticator.data.platform.manager.FeatureFlagManager
import com.bitwarden.authenticator.data.platform.manager.clipboard.BitwardenClipboardManager
import com.bitwarden.authenticator.data.platform.manager.model.FlagKey
import com.bitwarden.authenticator.data.platform.repository.SettingsRepository
import com.bitwarden.authenticator.data.platform.repository.model.BiometricsKeyResult
import com.bitwarden.authenticator.ui.platform.feature.settings.appearance.model.AppLanguage
@@ -49,7 +47,6 @@ class SettingsViewModel @Inject constructor(
private val authenticatorBridgeManager: AuthenticatorBridgeManager,
private val settingsRepository: SettingsRepository,
private val clipboardManager: BitwardenClipboardManager,
featureFlagManager: FeatureFlagManager,
) : BaseViewModel<SettingsState, SettingsEvent, SettingsAction>(
initialState = savedStateHandle[KEY_STATE]
?: createInitialState(
@@ -58,8 +55,6 @@ class SettingsViewModel @Inject constructor(
appTheme = settingsRepository.appTheme,
unlockWithBiometricsEnabled = settingsRepository.isUnlockWithBiometricsEnabled,
isSubmitCrashLogsEnabled = settingsRepository.isCrashLoggingEnabled,
isSyncWithBitwardenFeatureEnabled =
featureFlagManager.getFeatureFlag(FlagKey.PasswordManagerSync),
accountSyncState = authenticatorBridgeManager.accountSyncStateFlow.value,
defaultSaveOption = settingsRepository.defaultSaveOption,
sharedAccountsState = authenticatorRepository.sharedCodesStateFlow.value,
@@ -323,13 +318,12 @@ class SettingsViewModel @Inject constructor(
unlockWithBiometricsEnabled: Boolean,
isSubmitCrashLogsEnabled: Boolean,
accountSyncState: AccountSyncState,
isSyncWithBitwardenFeatureEnabled: Boolean,
sharedAccountsState: SharedVerificationCodesState,
): SettingsState {
val currentYear = Year.now(clock)
val copyrightInfo = "© Bitwarden Inc. 2015-$currentYear".asText()
// Show sync with Bitwarden row if feature is enabled and the OS is supported:
val shouldShowSyncWithBitwarden = isSyncWithBitwardenFeatureEnabled &&
// Show sync with Bitwarden row if the OS is supported:
val shouldShowSyncWithBitwarden =
accountSyncState != AccountSyncState.OsVersionNotSupported
// Show default save options only if the user had enabled sync with Bitwarden:
// (They can enable it via the "Sync with Bitwarden" row.

View File

@@ -115,7 +115,7 @@
<string name="download_bitwarden_card_message">Store all of your logins and sync verification codes directly with the Authenticator app.</string>
<string name="download_now">Download now</string>
<string name="sync_with_bitwarden_app">Sync with Bitwarden app</string>
<string name="this_feature_is_not_not_yet_available_for_self_hosted_users">This feature is not yet available for self-hosted users. <annotation link="learnMore">Learn more</annotation></string>
<string name="learn_more_link"><annotation link="learnMore">Learn more</annotation></string>
<string name="shared_codes_error">Unable to sync codes from the Bitwarden app. Make sure both apps are up-to-date. You can still access your existing codes in the Bitwarden app.</string>
<string name="shared_accounts_header">%1$s | %2$s (%3$d)</string>
<string name="sync_with_the_bitwarden_app">Sync with the Bitwarden app</string>

View File

@@ -14,6 +14,5 @@
<string name="debug_menu">Debug Menu</string>
<string name="reset_values">Reset values</string>
<string name="bitwarden_authentication_enabled">Bitwarden authentication enabled</string>
<string name="password_manager_sync">Password manager sync</string>
</resources>

View File

@@ -9,9 +9,7 @@ import com.bitwarden.authenticator.data.authenticator.manager.model.Verification
import com.bitwarden.authenticator.data.authenticator.repository.model.AuthenticatorItem
import com.bitwarden.authenticator.data.authenticator.repository.model.SharedVerificationCodesState
import com.bitwarden.authenticator.data.authenticator.repository.util.toAuthenticatorItems
import com.bitwarden.authenticator.data.platform.manager.FeatureFlagManager
import com.bitwarden.authenticator.data.platform.manager.imports.ImportManager
import com.bitwarden.authenticator.data.platform.manager.model.FlagKey
import com.bitwarden.authenticator.data.platform.repository.SettingsRepository
import com.bitwarden.authenticatorbridge.manager.AuthenticatorBridgeManager
import com.bitwarden.authenticatorbridge.manager.model.AccountSyncState
@@ -45,15 +43,6 @@ class AuthenticatorRepositoryTest {
private val mockFileManager = mockk<FileManager>()
private val mockImportManager = mockk<ImportManager>()
private val mockDispatcherManager = FakeDispatcherManager()
private val mutablePasswordSyncFlagStateFlow = MutableStateFlow(true)
private val mockFeatureFlagManager = mockk<FeatureFlagManager> {
every {
getFeatureFlagFlow(FlagKey.PasswordManagerSync)
} returns mutablePasswordSyncFlagStateFlow
every {
getFeatureFlag(FlagKey.PasswordManagerSync)
} returns mutablePasswordSyncFlagStateFlow.value
}
private val settingsRepository: SettingsRepository = mockk {
every { previouslySyncedBitwardenAccountIds } returns emptySet()
}
@@ -61,7 +50,6 @@ class AuthenticatorRepositoryTest {
private val authenticatorRepository = AuthenticatorRepositoryImpl(
authenticatorDiskSource = fakeAuthenticatorDiskSource,
authenticatorBridgeManager = mockAuthenticatorBridgeManager,
featureFlagManager = mockFeatureFlagManager,
totpCodeManager = mockTotpCodeManager,
fileManager = mockFileManager,
importManager = mockImportManager,
@@ -89,29 +77,6 @@ class AuthenticatorRepositoryTest {
}
}
@Test
fun `sharedCodesStateFlow value should be FeatureNotEnabled when feature flag is off`() =
runTest {
val repository = AuthenticatorRepositoryImpl(
authenticatorDiskSource = fakeAuthenticatorDiskSource,
authenticatorBridgeManager = mockAuthenticatorBridgeManager,
featureFlagManager = mockFeatureFlagManager,
totpCodeManager = mockTotpCodeManager,
fileManager = mockFileManager,
importManager = mockImportManager,
dispatcherManager = mockDispatcherManager,
settingRepository = settingsRepository,
)
mutablePasswordSyncFlagStateFlow.value = false
mutableAccountSyncStateFlow.value = AccountSyncState.Success(emptyList())
repository.sharedCodesStateFlow.test {
assertEquals(
SharedVerificationCodesState.FeatureNotEnabled,
awaitItem(),
)
}
}
@Test
fun `ciphersStateFlow should emit sorted authenticator items when disk source changes`() =
runTest {
@@ -123,27 +88,6 @@ class AuthenticatorRepositoryTest {
)
}
@Test
fun `sharedCodesStateFlow should emit FeatureNotEnabled when feature flag is off`() = runTest {
val repository = AuthenticatorRepositoryImpl(
authenticatorDiskSource = fakeAuthenticatorDiskSource,
authenticatorBridgeManager = mockAuthenticatorBridgeManager,
featureFlagManager = mockFeatureFlagManager,
totpCodeManager = mockTotpCodeManager,
fileManager = mockFileManager,
importManager = mockImportManager,
dispatcherManager = mockDispatcherManager,
settingRepository = settingsRepository,
)
mutablePasswordSyncFlagStateFlow.value = false
repository.sharedCodesStateFlow.test {
assertEquals(
SharedVerificationCodesState.FeatureNotEnabled,
awaitItem(),
)
}
}
@Suppress("MaxLineLength")
@Test
fun `sharedCodesStateFlow should emit AppNotInstalled when authenticatorBridgeManager emits AppNotInstalled`() =

View File

@@ -73,7 +73,7 @@ class FeatureFlagManagerTest {
)
val flagValue = manager.getFeatureFlag(
key = FlagKey.PasswordManagerSync,
key = FlagKey.DummyBoolean,
forceRefresh = false,
)
assertFalse(flagValue)
@@ -169,7 +169,7 @@ class FeatureFlagManagerTest {
fakeServerConfigRepository.serverConfigValue = null
val flagValue = manager.getFeatureFlag(
key = FlagKey.PasswordManagerSync,
key = FlagKey.DummyBoolean,
forceRefresh = false,
)

View File

@@ -12,10 +12,6 @@ class FlagKeyTest {
FlagKey.BitwardenAuthenticationEnabled.keyName,
"bitwarden-authentication-enabled",
)
assertEquals(
FlagKey.PasswordManagerSync.keyName,
"enable-pm-bwa-sync",
)
}
@Test
@@ -23,7 +19,6 @@ class FlagKeyTest {
assertTrue(
listOf(
FlagKey.BitwardenAuthenticationEnabled,
FlagKey.PasswordManagerSync,
).all {
!it.defaultValue
},

View File

@@ -98,10 +98,6 @@ class DebugMenuRepositoryTest {
runTest {
debugMenuRepository.resetFeatureFlagOverrides()
verify(exactly = 1) {
mockFeatureFlagOverrideDiskSource.saveFeatureFlag(
FlagKey.PasswordManagerSync,
FlagKey.PasswordManagerSync.defaultValue,
)
mockFeatureFlagOverrideDiskSource.saveFeatureFlag(
FlagKey.BitwardenAuthenticationEnabled,
FlagKey.BitwardenAuthenticationEnabled.defaultValue,

View File

@@ -52,7 +52,7 @@ class DebugMenuScreenTest : AuthenticatorComposeTest() {
@Test
fun `feature flag content should not display if the state is empty`() {
composeTestRule
.onNodeWithText("Password manager sync", ignoreCase = true)
.onNodeWithText("Bitwarden authentication enabled", ignoreCase = true)
.assertDoesNotExist()
}
@@ -61,13 +61,13 @@ class DebugMenuScreenTest : AuthenticatorComposeTest() {
mutableStateFlow.tryEmit(
DebugMenuState(
featureFlags = mapOf(
FlagKey.PasswordManagerSync to true,
FlagKey.BitwardenAuthenticationEnabled to true,
),
),
)
composeTestRule
.onNodeWithText("Password manager sync", ignoreCase = true)
.onNodeWithText("Bitwarden authentication enabled", ignoreCase = true)
.assertExists()
}
@@ -76,18 +76,18 @@ class DebugMenuScreenTest : AuthenticatorComposeTest() {
mutableStateFlow.tryEmit(
DebugMenuState(
featureFlags = mapOf(
FlagKey.PasswordManagerSync to true,
FlagKey.BitwardenAuthenticationEnabled to true,
),
),
)
composeTestRule
.onNodeWithText("Password manager sync", ignoreCase = true)
.onNodeWithText("Bitwarden authentication enabled", ignoreCase = true)
.performClick()
verify {
viewModel.trySendAction(
DebugMenuAction.UpdateFeatureFlag(
FlagKey.PasswordManagerSync,
FlagKey.BitwardenAuthenticationEnabled,
false,
),
)

View File

@@ -64,9 +64,11 @@ class DebugMenuViewModelTest : BaseViewModelTest() {
fun `handleUpdateFeatureFlag should update the feature flag via the repository`() {
val viewModel = createViewModel()
viewModel.trySendAction(
DebugMenuAction.UpdateFeatureFlag(FlagKey.PasswordManagerSync, false),
DebugMenuAction.UpdateFeatureFlag(FlagKey.BitwardenAuthenticationEnabled, false),
)
verify { mockDebugMenuRepository.updateFeatureFlag(FlagKey.PasswordManagerSync, false) }
verify {
mockDebugMenuRepository.updateFeatureFlag(FlagKey.BitwardenAuthenticationEnabled, false)
}
}
private fun createViewModel(): DebugMenuViewModel = DebugMenuViewModel(
@@ -77,12 +79,10 @@ class DebugMenuViewModelTest : BaseViewModelTest() {
private val DEFAULT_MAP_VALUE: Map<FlagKey<Any>, Any> = mapOf(
FlagKey.BitwardenAuthenticationEnabled to true,
FlagKey.PasswordManagerSync to true,
)
private val UPDATED_MAP_VALUE: Map<FlagKey<Any>, Any> = mapOf(
FlagKey.BitwardenAuthenticationEnabled to false,
FlagKey.PasswordManagerSync to false,
)
private val DEFAULT_STATE = DebugMenuState(

View File

@@ -7,9 +7,7 @@ import com.bitwarden.authenticator.R
import com.bitwarden.authenticator.data.authenticator.repository.AuthenticatorRepository
import com.bitwarden.authenticator.data.authenticator.repository.model.SharedVerificationCodesState
import com.bitwarden.authenticator.data.authenticator.repository.util.isSyncWithBitwardenEnabled
import com.bitwarden.authenticator.data.platform.manager.FeatureFlagManager
import com.bitwarden.authenticator.data.platform.manager.clipboard.BitwardenClipboardManager
import com.bitwarden.authenticator.data.platform.manager.model.FlagKey
import com.bitwarden.authenticator.data.platform.repository.SettingsRepository
import com.bitwarden.authenticator.ui.platform.feature.settings.appearance.model.AppLanguage
import com.bitwarden.authenticator.ui.platform.feature.settings.data.model.DefaultSaveOption
@@ -58,9 +56,6 @@ class SettingsViewModelTest : BaseViewModelTest() {
every { isCrashLoggingEnabled } returns true
}
private val clipboardManager: BitwardenClipboardManager = mockk()
private val featureFlagManager: FeatureFlagManager = mockk {
every { getFeatureFlag(FlagKey.PasswordManagerSync) } returns true
}
@BeforeEach
fun setup() {
@@ -74,25 +69,7 @@ class SettingsViewModelTest : BaseViewModelTest() {
}
@Test
@Suppress("MaxLineLength")
fun `initialState should be correct when saved state is null and password manager feature flag is off`() {
every {
featureFlagManager.getFeatureFlag(FlagKey.PasswordManagerSync)
} returns false
val viewModel = createViewModel(savedState = null)
val expectedState = DEFAULT_STATE.copy(
showSyncWithBitwarden = false,
showDefaultSaveOptionRow = false,
)
assertEquals(
expectedState,
viewModel.stateFlow.value,
)
}
@Test
@Suppress("MaxLineLength")
fun `initialState should be correct when saved state is null and password manager feature flag is on but OS version is too low`() {
fun `initialState should be correct when saved state is null but OS version is too low`() {
every {
authenticatorBridgeManager.accountSyncStateFlow
} returns MutableStateFlow(AccountSyncState.OsVersionNotSupported)
@@ -108,14 +85,10 @@ class SettingsViewModelTest : BaseViewModelTest() {
}
@Test
@Suppress("MaxLineLength")
fun `initialState should be correct when saved state is null and password manager feature flag is on and OS version is supported`() {
fun `initialState should be correct when saved state is null and OS version is supported`() {
every {
authenticatorBridgeManager.accountSyncStateFlow
} returns MutableStateFlow(AccountSyncState.Loading)
every {
featureFlagManager.getFeatureFlag(FlagKey.PasswordManagerSync)
} returns true
val viewModel = createViewModel(savedState = null)
val expectedState = DEFAULT_STATE.copy(
showSyncWithBitwarden = true,
@@ -233,7 +206,6 @@ class SettingsViewModelTest : BaseViewModelTest() {
authenticatorRepository = authenticatorRepository,
settingsRepository = settingsRepository,
clipboardManager = clipboardManager,
featureFlagManager = featureFlagManager,
)
}