diff --git a/authenticator/libs/authenticatorbridge-1.0.0-release.aar b/authenticator/libs/authenticatorbridge-1.0.0-release.aar index 58a7c494b0..bc793ddbb5 100644 Binary files a/authenticator/libs/authenticatorbridge-1.0.0-release.aar and b/authenticator/libs/authenticatorbridge-1.0.0-release.aar differ diff --git a/authenticator/src/main/kotlin/com/bitwarden/authenticator/data/authenticator/repository/AuthenticatorRepositoryImpl.kt b/authenticator/src/main/kotlin/com/bitwarden/authenticator/data/authenticator/repository/AuthenticatorRepositoryImpl.kt index 0804a0b35d..d577506c3e 100644 --- a/authenticator/src/main/kotlin/com/bitwarden/authenticator/data/authenticator/repository/AuthenticatorRepositoryImpl.kt +++ b/authenticator/src/main/kotlin/com/bitwarden/authenticator/data/authenticator/repository/AuthenticatorRepositoryImpl.kt @@ -40,6 +40,7 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.flow.flatMapConcat import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.launchIn @@ -155,44 +156,22 @@ class AuthenticatorRepositoryImpl @Inject constructor( @OptIn(ExperimentalCoroutinesApi::class) override val sharedCodesStateFlow: StateFlow by lazy { - if (!featureFlagManager.getFeatureFlag(FlagKey.PasswordManagerSync)) { - MutableStateFlow(SharedVerificationCodesState.FeatureNotEnabled) - } else { - authenticatorBridgeManager - .accountSyncStateFlow - .flatMapLatest { accountSyncState -> - when (accountSyncState) { - AccountSyncState.AppNotInstalled -> - MutableStateFlow(SharedVerificationCodesState.AppNotInstalled) - - AccountSyncState.SyncNotEnabled -> - MutableStateFlow(SharedVerificationCodesState.SyncNotEnabled) - - AccountSyncState.Error -> - MutableStateFlow(SharedVerificationCodesState.Error) - - AccountSyncState.Loading -> - MutableStateFlow(SharedVerificationCodesState.Loading) - - AccountSyncState.OsVersionNotSupported -> MutableStateFlow( - SharedVerificationCodesState.OsVersionNotSupported, - ) - - is AccountSyncState.Success -> { - val verificationCodesList = - accountSyncState.accounts.toAuthenticatorItems() - totpCodeManager - .getTotpCodesFlow(verificationCodesList) - .map { SharedVerificationCodesState.Success(it) } - } - } + featureFlagManager + .getFeatureFlagFlow(FlagKey.PasswordManagerSync) + .flatMapLatest { isFeatureEnabled -> + if (isFeatureEnabled) { + authenticatorBridgeManager + .accountSyncStateFlow + .flatMapConcat { it.toSharedVerificationCodesStateFlow() } + } else { + flowOf(SharedVerificationCodesState.FeatureNotEnabled) } - .stateIn( - scope = unconfinedScope, - started = SharingStarted.WhileSubscribed(), - initialValue = SharedVerificationCodesState.Loading, - ) - } + } + .stateIn( + scope = unconfinedScope, + started = SharingStarted.WhileSubscribed(STOP_TIMEOUT_DELAY_MS), + initialValue = SharedVerificationCodesState.Loading, + ) } @OptIn(ExperimentalCoroutinesApi::class) @@ -298,6 +277,33 @@ class AuthenticatorRepositoryImpl @Inject constructor( override val firstTimeAccountSyncFlow: Flow get() = firstTimeAccountSyncChannel.receiveAsFlow() + @Suppress("MaxLineLength") + private fun AccountSyncState.toSharedVerificationCodesStateFlow(): Flow = + when (this) { + AccountSyncState.AppNotInstalled -> + flowOf(SharedVerificationCodesState.AppNotInstalled) + + AccountSyncState.SyncNotEnabled -> + flowOf(SharedVerificationCodesState.SyncNotEnabled) + + AccountSyncState.Error -> + flowOf(SharedVerificationCodesState.Error) + + AccountSyncState.Loading -> + flowOf(SharedVerificationCodesState.Loading) + + AccountSyncState.OsVersionNotSupported -> flowOf( + SharedVerificationCodesState.OsVersionNotSupported, + ) + + is AccountSyncState.Success -> { + val verificationCodesList = accounts.toAuthenticatorItems() + totpCodeManager + .getTotpCodesFlow(verificationCodesList) + .map { SharedVerificationCodesState.Success(it) } + } + } + private suspend fun encodeVaultDataToCsv(fileUri: Uri): ExportDataResult { val headerLine = "folder,favorite,type,name,login_uri,login_totp" diff --git a/authenticator/src/test/java/com/bitwarden/authenticator/data/authenticator/repository/AuthenticatorRepositoryTest.kt b/authenticator/src/test/java/com/bitwarden/authenticator/data/authenticator/repository/AuthenticatorRepositoryTest.kt index 5a8d3dcf4a..33000f3f05 100644 --- a/authenticator/src/test/java/com/bitwarden/authenticator/data/authenticator/repository/AuthenticatorRepositoryTest.kt +++ b/authenticator/src/test/java/com/bitwarden/authenticator/data/authenticator/repository/AuthenticatorRepositoryTest.kt @@ -45,8 +45,14 @@ class AuthenticatorRepositoryTest { private val mockFileManager = mockk() private val mockImportManager = mockk() private val mockDispatcherManager = FakeDispatcherManager() + private val mutablePasswordSyncFlagStateFlow = MutableStateFlow(true) private val mockFeatureFlagManager = mockk { - every { getFeatureFlag(FlagKey.PasswordManagerSync) } returns true + every { + getFeatureFlagFlow(FlagKey.PasswordManagerSync) + } returns mutablePasswordSyncFlagStateFlow + every { + getFeatureFlag(FlagKey.PasswordManagerSync) + } returns mutablePasswordSyncFlagStateFlow.value } private val settingsRepository: SettingsRepository = mockk { every { previouslySyncedBitwardenAccountIds } returns emptySet() @@ -84,25 +90,27 @@ class AuthenticatorRepositoryTest { } @Test - fun `sharedCodesStateFlow value should be FeatureNotEnabled when feature flag is off`() { - every { - mockFeatureFlagManager.getFeatureFlag(FlagKey.PasswordManagerSync) - } returns false - val repository = AuthenticatorRepositoryImpl( - authenticatorDiskSource = fakeAuthenticatorDiskSource, - authenticatorBridgeManager = mockAuthenticatorBridgeManager, - featureFlagManager = mockFeatureFlagManager, - totpCodeManager = mockTotpCodeManager, - fileManager = mockFileManager, - importManager = mockImportManager, - dispatcherManager = mockDispatcherManager, - settingRepository = settingsRepository, - ) - assertEquals( - SharedVerificationCodesState.FeatureNotEnabled, - repository.sharedCodesStateFlow.value, - ) - } + 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`() = @@ -117,9 +125,6 @@ class AuthenticatorRepositoryTest { @Test fun `sharedCodesStateFlow should emit FeatureNotEnabled when feature flag is off`() = runTest { - every { - mockFeatureFlagManager.getFeatureFlag(FlagKey.PasswordManagerSync) - } returns false val repository = AuthenticatorRepositoryImpl( authenticatorDiskSource = fakeAuthenticatorDiskSource, authenticatorBridgeManager = mockAuthenticatorBridgeManager, @@ -130,6 +135,7 @@ class AuthenticatorRepositoryTest { dispatcherManager = mockDispatcherManager, settingRepository = settingsRepository, ) + mutablePasswordSyncFlagStateFlow.value = false repository.sharedCodesStateFlow.test { assertEquals( SharedVerificationCodesState.FeatureNotEnabled, diff --git a/authenticatorbridge/src/main/java/com/bitwarden/authenticatorbridge/manager/AuthenticatorBridgeManagerImpl.kt b/authenticatorbridge/src/main/java/com/bitwarden/authenticatorbridge/manager/AuthenticatorBridgeManagerImpl.kt index 95116ce5ab..e9edccf509 100644 --- a/authenticatorbridge/src/main/java/com/bitwarden/authenticatorbridge/manager/AuthenticatorBridgeManagerImpl.kt +++ b/authenticatorbridge/src/main/java/com/bitwarden/authenticatorbridge/manager/AuthenticatorBridgeManagerImpl.kt @@ -156,6 +156,13 @@ internal class AuthenticatorBridgeManagerImpl( if (!isBound) { mutableSharedAccountsStateFlow.value = AccountSyncState.Error + } else if (mutableSharedAccountsStateFlow.value == AccountSyncState.AppNotInstalled) { + // This scenario occurs when the Authenticator is installed before Bitwarden, because + // `AppNotInstalled` is the initial state. Binding to the service simply means Bitwarden + // is installed, but does not indicate whether syncing is enabled. When/if syncing is + // toggled in Bitwarden, `onServiceConnected` will be invoked and the state + // will be updated. + mutableSharedAccountsStateFlow.value = AccountSyncState.SyncNotEnabled } }