Skip to content

Commit 54efc74

Browse files
authored
[PM-21385] Defer feature flag check for Bitwarden account sync (#5222)
1 parent 34aed2a commit 54efc74

File tree

4 files changed

+79
-60
lines changed

4 files changed

+79
-60
lines changed
Binary file not shown.

authenticator/src/main/kotlin/com/bitwarden/authenticator/data/authenticator/repository/AuthenticatorRepositoryImpl.kt

Lines changed: 43 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import kotlinx.coroutines.flow.StateFlow
4040
import kotlinx.coroutines.flow.asSharedFlow
4141
import kotlinx.coroutines.flow.asStateFlow
4242
import kotlinx.coroutines.flow.firstOrNull
43+
import kotlinx.coroutines.flow.flatMapConcat
4344
import kotlinx.coroutines.flow.flatMapLatest
4445
import kotlinx.coroutines.flow.flowOf
4546
import kotlinx.coroutines.flow.launchIn
@@ -155,44 +156,22 @@ class AuthenticatorRepositoryImpl @Inject constructor(
155156

156157
@OptIn(ExperimentalCoroutinesApi::class)
157158
override val sharedCodesStateFlow: StateFlow<SharedVerificationCodesState> by lazy {
158-
if (!featureFlagManager.getFeatureFlag(FlagKey.PasswordManagerSync)) {
159-
MutableStateFlow(SharedVerificationCodesState.FeatureNotEnabled)
160-
} else {
161-
authenticatorBridgeManager
162-
.accountSyncStateFlow
163-
.flatMapLatest { accountSyncState ->
164-
when (accountSyncState) {
165-
AccountSyncState.AppNotInstalled ->
166-
MutableStateFlow(SharedVerificationCodesState.AppNotInstalled)
167-
168-
AccountSyncState.SyncNotEnabled ->
169-
MutableStateFlow(SharedVerificationCodesState.SyncNotEnabled)
170-
171-
AccountSyncState.Error ->
172-
MutableStateFlow(SharedVerificationCodesState.Error)
173-
174-
AccountSyncState.Loading ->
175-
MutableStateFlow(SharedVerificationCodesState.Loading)
176-
177-
AccountSyncState.OsVersionNotSupported -> MutableStateFlow(
178-
SharedVerificationCodesState.OsVersionNotSupported,
179-
)
180-
181-
is AccountSyncState.Success -> {
182-
val verificationCodesList =
183-
accountSyncState.accounts.toAuthenticatorItems()
184-
totpCodeManager
185-
.getTotpCodesFlow(verificationCodesList)
186-
.map { SharedVerificationCodesState.Success(it) }
187-
}
188-
}
159+
featureFlagManager
160+
.getFeatureFlagFlow(FlagKey.PasswordManagerSync)
161+
.flatMapLatest { isFeatureEnabled ->
162+
if (isFeatureEnabled) {
163+
authenticatorBridgeManager
164+
.accountSyncStateFlow
165+
.flatMapConcat { it.toSharedVerificationCodesStateFlow() }
166+
} else {
167+
flowOf(SharedVerificationCodesState.FeatureNotEnabled)
189168
}
190-
.stateIn(
191-
scope = unconfinedScope,
192-
started = SharingStarted.WhileSubscribed(),
193-
initialValue = SharedVerificationCodesState.Loading,
194-
)
195-
}
169+
}
170+
.stateIn(
171+
scope = unconfinedScope,
172+
started = SharingStarted.WhileSubscribed(STOP_TIMEOUT_DELAY_MS),
173+
initialValue = SharedVerificationCodesState.Loading,
174+
)
196175
}
197176

198177
@OptIn(ExperimentalCoroutinesApi::class)
@@ -298,6 +277,33 @@ class AuthenticatorRepositoryImpl @Inject constructor(
298277
override val firstTimeAccountSyncFlow: Flow<Unit>
299278
get() = firstTimeAccountSyncChannel.receiveAsFlow()
300279

280+
@Suppress("MaxLineLength")
281+
private fun AccountSyncState.toSharedVerificationCodesStateFlow(): Flow<SharedVerificationCodesState> =
282+
when (this) {
283+
AccountSyncState.AppNotInstalled ->
284+
flowOf(SharedVerificationCodesState.AppNotInstalled)
285+
286+
AccountSyncState.SyncNotEnabled ->
287+
flowOf(SharedVerificationCodesState.SyncNotEnabled)
288+
289+
AccountSyncState.Error ->
290+
flowOf(SharedVerificationCodesState.Error)
291+
292+
AccountSyncState.Loading ->
293+
flowOf(SharedVerificationCodesState.Loading)
294+
295+
AccountSyncState.OsVersionNotSupported -> flowOf(
296+
SharedVerificationCodesState.OsVersionNotSupported,
297+
)
298+
299+
is AccountSyncState.Success -> {
300+
val verificationCodesList = accounts.toAuthenticatorItems()
301+
totpCodeManager
302+
.getTotpCodesFlow(verificationCodesList)
303+
.map { SharedVerificationCodesState.Success(it) }
304+
}
305+
}
306+
301307
private suspend fun encodeVaultDataToCsv(fileUri: Uri): ExportDataResult {
302308
val headerLine =
303309
"folder,favorite,type,name,login_uri,login_totp"

authenticator/src/test/java/com/bitwarden/authenticator/data/authenticator/repository/AuthenticatorRepositoryTest.kt

Lines changed: 29 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,14 @@ class AuthenticatorRepositoryTest {
4545
private val mockFileManager = mockk<FileManager>()
4646
private val mockImportManager = mockk<ImportManager>()
4747
private val mockDispatcherManager = FakeDispatcherManager()
48+
private val mutablePasswordSyncFlagStateFlow = MutableStateFlow(true)
4849
private val mockFeatureFlagManager = mockk<FeatureFlagManager> {
49-
every { getFeatureFlag(FlagKey.PasswordManagerSync) } returns true
50+
every {
51+
getFeatureFlagFlow(FlagKey.PasswordManagerSync)
52+
} returns mutablePasswordSyncFlagStateFlow
53+
every {
54+
getFeatureFlag(FlagKey.PasswordManagerSync)
55+
} returns mutablePasswordSyncFlagStateFlow.value
5056
}
5157
private val settingsRepository: SettingsRepository = mockk {
5258
every { previouslySyncedBitwardenAccountIds } returns emptySet()
@@ -84,25 +90,27 @@ class AuthenticatorRepositoryTest {
8490
}
8591

8692
@Test
87-
fun `sharedCodesStateFlow value should be FeatureNotEnabled when feature flag is off`() {
88-
every {
89-
mockFeatureFlagManager.getFeatureFlag(FlagKey.PasswordManagerSync)
90-
} returns false
91-
val repository = AuthenticatorRepositoryImpl(
92-
authenticatorDiskSource = fakeAuthenticatorDiskSource,
93-
authenticatorBridgeManager = mockAuthenticatorBridgeManager,
94-
featureFlagManager = mockFeatureFlagManager,
95-
totpCodeManager = mockTotpCodeManager,
96-
fileManager = mockFileManager,
97-
importManager = mockImportManager,
98-
dispatcherManager = mockDispatcherManager,
99-
settingRepository = settingsRepository,
100-
)
101-
assertEquals(
102-
SharedVerificationCodesState.FeatureNotEnabled,
103-
repository.sharedCodesStateFlow.value,
104-
)
105-
}
93+
fun `sharedCodesStateFlow value should be FeatureNotEnabled when feature flag is off`() =
94+
runTest {
95+
val repository = AuthenticatorRepositoryImpl(
96+
authenticatorDiskSource = fakeAuthenticatorDiskSource,
97+
authenticatorBridgeManager = mockAuthenticatorBridgeManager,
98+
featureFlagManager = mockFeatureFlagManager,
99+
totpCodeManager = mockTotpCodeManager,
100+
fileManager = mockFileManager,
101+
importManager = mockImportManager,
102+
dispatcherManager = mockDispatcherManager,
103+
settingRepository = settingsRepository,
104+
)
105+
mutablePasswordSyncFlagStateFlow.value = false
106+
mutableAccountSyncStateFlow.value = AccountSyncState.Success(emptyList())
107+
repository.sharedCodesStateFlow.test {
108+
assertEquals(
109+
SharedVerificationCodesState.FeatureNotEnabled,
110+
awaitItem(),
111+
)
112+
}
113+
}
106114

107115
@Test
108116
fun `ciphersStateFlow should emit sorted authenticator items when disk source changes`() =
@@ -117,9 +125,6 @@ class AuthenticatorRepositoryTest {
117125

118126
@Test
119127
fun `sharedCodesStateFlow should emit FeatureNotEnabled when feature flag is off`() = runTest {
120-
every {
121-
mockFeatureFlagManager.getFeatureFlag(FlagKey.PasswordManagerSync)
122-
} returns false
123128
val repository = AuthenticatorRepositoryImpl(
124129
authenticatorDiskSource = fakeAuthenticatorDiskSource,
125130
authenticatorBridgeManager = mockAuthenticatorBridgeManager,
@@ -130,6 +135,7 @@ class AuthenticatorRepositoryTest {
130135
dispatcherManager = mockDispatcherManager,
131136
settingRepository = settingsRepository,
132137
)
138+
mutablePasswordSyncFlagStateFlow.value = false
133139
repository.sharedCodesStateFlow.test {
134140
assertEquals(
135141
SharedVerificationCodesState.FeatureNotEnabled,

authenticatorbridge/src/main/java/com/bitwarden/authenticatorbridge/manager/AuthenticatorBridgeManagerImpl.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,13 @@ internal class AuthenticatorBridgeManagerImpl(
156156

157157
if (!isBound) {
158158
mutableSharedAccountsStateFlow.value = AccountSyncState.Error
159+
} else if (mutableSharedAccountsStateFlow.value == AccountSyncState.AppNotInstalled) {
160+
// This scenario occurs when the Authenticator is installed before Bitwarden, because
161+
// `AppNotInstalled` is the initial state. Binding to the service simply means Bitwarden
162+
// is installed, but does not indicate whether syncing is enabled. When/if syncing is
163+
// toggled in Bitwarden, `onServiceConnected` will be invoked and the state
164+
// will be updated.
165+
mutableSharedAccountsStateFlow.value = AccountSyncState.SyncNotEnabled
159166
}
160167
}
161168

0 commit comments

Comments
 (0)