Skip to content

Commit dc198ea

Browse files
PM-25125: Refactor user state managment into UserStateManager (#5774)
1 parent ff23dc3 commit dc198ea

File tree

11 files changed

+689
-483
lines changed

11 files changed

+689
-483
lines changed
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package com.x8bit.bitwarden.data.auth.manager
2+
3+
import com.x8bit.bitwarden.data.auth.repository.model.UserState
4+
import kotlinx.coroutines.flow.StateFlow
5+
6+
/**
7+
* Manages the global state of all users.
8+
*/
9+
interface UserStateManager {
10+
/**
11+
* Emits updates for changes to the [UserState].
12+
*/
13+
val userStateFlow: StateFlow<UserState?>
14+
15+
/**
16+
* Tracks whether there is an additional account that is pending login/registration in order to
17+
* have multiple accounts available.
18+
*
19+
* This allows a direct view into and modification of [UserState.hasPendingAccountAddition].
20+
* Note that this call has no effect when there is no [UserState] information available.
21+
*/
22+
var hasPendingAccountAddition: Boolean
23+
24+
/**
25+
* Emits updates for changes to the [UserState.hasPendingAccountAddition] flag.
26+
*/
27+
val hasPendingAccountAdditionStateFlow: StateFlow<Boolean>
28+
29+
/**
30+
* Tracks whether there is an account that is pending deletion in order to allow the account to
31+
* remain active until the deletion is finalized.
32+
*/
33+
var hasPendingAccountDeletion: Boolean
34+
35+
/**
36+
* Run the given [block] while preventing any updates to [UserState]. This is useful in cases
37+
* where many individual changes might occur that would normally affect the [UserState] but we
38+
* only want a single final emission. In the rare case that multiple threads are running
39+
* transactions simultaneously, there will be no [UserState] updates until the last
40+
* transaction completes.
41+
*/
42+
suspend fun <T> userStateTransaction(block: suspend () -> T): T
43+
}
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
package com.x8bit.bitwarden.data.auth.manager
2+
3+
import com.bitwarden.data.manager.DispatcherManager
4+
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
5+
import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus
6+
import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson
7+
import com.x8bit.bitwarden.data.auth.repository.model.UserAccountTokens
8+
import com.x8bit.bitwarden.data.auth.repository.model.UserKeyConnectorState
9+
import com.x8bit.bitwarden.data.auth.repository.model.UserOrganizations
10+
import com.x8bit.bitwarden.data.auth.repository.model.UserState
11+
import com.x8bit.bitwarden.data.auth.repository.model.VaultUnlockType
12+
import com.x8bit.bitwarden.data.auth.repository.util.currentOnboardingStatus
13+
import com.x8bit.bitwarden.data.auth.repository.util.onboardingStatusChangesFlow
14+
import com.x8bit.bitwarden.data.auth.repository.util.toUserState
15+
import com.x8bit.bitwarden.data.auth.repository.util.userAccountTokens
16+
import com.x8bit.bitwarden.data.auth.repository.util.userAccountTokensFlow
17+
import com.x8bit.bitwarden.data.auth.repository.util.userKeyConnectorStateFlow
18+
import com.x8bit.bitwarden.data.auth.repository.util.userKeyConnectorStateList
19+
import com.x8bit.bitwarden.data.auth.repository.util.userOrganizationsList
20+
import com.x8bit.bitwarden.data.auth.repository.util.userOrganizationsListFlow
21+
import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManager
22+
import com.x8bit.bitwarden.data.platform.manager.model.FirstTimeState
23+
import com.x8bit.bitwarden.data.vault.manager.VaultLockManager
24+
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockData
25+
import kotlinx.coroutines.CoroutineScope
26+
import kotlinx.coroutines.flow.MutableStateFlow
27+
import kotlinx.coroutines.flow.SharingStarted
28+
import kotlinx.coroutines.flow.StateFlow
29+
import kotlinx.coroutines.flow.combine
30+
import kotlinx.coroutines.flow.filterNot
31+
import kotlinx.coroutines.flow.merge
32+
import kotlinx.coroutines.flow.stateIn
33+
import kotlinx.coroutines.flow.update
34+
35+
/**
36+
* The default implementation of the [UserStateManager].
37+
*/
38+
class UserStateManagerImpl(
39+
private val authDiskSource: AuthDiskSource,
40+
firstTimeActionManager: FirstTimeActionManager,
41+
vaultLockManager: VaultLockManager,
42+
dispatcherManager: DispatcherManager,
43+
) : UserStateManager {
44+
private val unconfinedScope = CoroutineScope(dispatcherManager.unconfined)
45+
46+
//region Pending Account Addition
47+
private val mutableHasPendingAccountAdditionStateFlow = MutableStateFlow(value = false)
48+
49+
override val hasPendingAccountAdditionStateFlow: StateFlow<Boolean>
50+
get() = mutableHasPendingAccountAdditionStateFlow
51+
52+
override var hasPendingAccountAddition: Boolean
53+
by mutableHasPendingAccountAdditionStateFlow::value
54+
//endregion Pending Account Addition
55+
56+
//region Pending Account Deletion
57+
/**
58+
* If there is a pending account deletion, continue showing the original UserState until it
59+
* is confirmed. This is accomplished by blocking the emissions of the [userStateFlow]
60+
* whenever set to `true`.
61+
*/
62+
private val mutableHasPendingAccountDeletionStateFlow = MutableStateFlow(value = false)
63+
64+
override var hasPendingAccountDeletion: Boolean
65+
by mutableHasPendingAccountDeletionStateFlow::value
66+
//endregion Pending Account Deletion
67+
68+
/**
69+
* Whenever a function needs to update multiple underlying data-points that contribute to the
70+
* [UserState], we update this [MutableStateFlow] and continue to show the original `UserState`
71+
* until the transaction is complete. This is accomplished by blocking the emissions of the
72+
* [userStateFlow] whenever this is set to a value above 0 (a count is used if more than one
73+
* process is updating data simultaneously).
74+
*/
75+
private val mutableUserStateTransactionCountStateFlow = MutableStateFlow(0)
76+
77+
@Suppress("UNCHECKED_CAST", "MagicNumber")
78+
override val userStateFlow: StateFlow<UserState?> = combine(
79+
authDiskSource.userStateFlow,
80+
authDiskSource.userAccountTokensFlow,
81+
authDiskSource.userOrganizationsListFlow,
82+
authDiskSource.userKeyConnectorStateFlow,
83+
authDiskSource.onboardingStatusChangesFlow,
84+
firstTimeActionManager.firstTimeStateFlow,
85+
vaultLockManager.vaultUnlockDataStateFlow,
86+
hasPendingAccountAdditionStateFlow,
87+
// Ignore the data in the merge, but trigger an update when they emit.
88+
merge(
89+
mutableHasPendingAccountDeletionStateFlow,
90+
mutableUserStateTransactionCountStateFlow,
91+
vaultLockManager.isActiveUserUnlockingFlow,
92+
),
93+
) { array ->
94+
val userStateJson = array[0] as UserStateJson?
95+
val userAccountTokens = array[1] as List<UserAccountTokens>
96+
val userOrganizationsList = array[2] as List<UserOrganizations>
97+
val userIsUsingKeyConnectorList = array[3] as List<UserKeyConnectorState>
98+
val onboardingStatus = array[4] as OnboardingStatus?
99+
val firstTimeState = array[5] as FirstTimeState
100+
val vaultState = array[6] as List<VaultUnlockData>
101+
val hasPendingAccountAddition = array[7] as Boolean
102+
userStateJson?.toUserState(
103+
vaultState = vaultState,
104+
userAccountTokens = userAccountTokens,
105+
userOrganizationsList = userOrganizationsList,
106+
userIsUsingKeyConnectorList = userIsUsingKeyConnectorList,
107+
hasPendingAccountAddition = hasPendingAccountAddition,
108+
onboardingStatus = onboardingStatus,
109+
isBiometricsEnabledProvider = ::isBiometricsEnabled,
110+
vaultUnlockTypeProvider = ::getVaultUnlockType,
111+
isDeviceTrustedProvider = ::isDeviceTrusted,
112+
firstTimeState = firstTimeState,
113+
)
114+
}
115+
.filterNot {
116+
mutableHasPendingAccountDeletionStateFlow.value ||
117+
mutableUserStateTransactionCountStateFlow.value > 0 ||
118+
vaultLockManager.isActiveUserUnlockingFlow.value
119+
}
120+
.stateIn(
121+
scope = unconfinedScope,
122+
started = SharingStarted.Eagerly,
123+
initialValue = authDiskSource
124+
.userState
125+
?.toUserState(
126+
vaultState = vaultLockManager.vaultUnlockDataStateFlow.value,
127+
userAccountTokens = authDiskSource.userAccountTokens,
128+
userOrganizationsList = authDiskSource.userOrganizationsList,
129+
userIsUsingKeyConnectorList = authDiskSource.userKeyConnectorStateList,
130+
hasPendingAccountAddition = mutableHasPendingAccountAdditionStateFlow.value,
131+
onboardingStatus = authDiskSource.currentOnboardingStatus,
132+
isBiometricsEnabledProvider = ::isBiometricsEnabled,
133+
vaultUnlockTypeProvider = ::getVaultUnlockType,
134+
isDeviceTrustedProvider = ::isDeviceTrusted,
135+
firstTimeState = firstTimeActionManager.currentOrDefaultUserFirstTimeState,
136+
),
137+
)
138+
139+
override suspend fun <T> userStateTransaction(block: suspend () -> T): T {
140+
mutableUserStateTransactionCountStateFlow.update { it.inc() }
141+
return try {
142+
block()
143+
} finally {
144+
mutableUserStateTransactionCountStateFlow.update { it.dec() }
145+
}
146+
}
147+
148+
private fun isBiometricsEnabled(
149+
userId: String,
150+
): Boolean = authDiskSource.getUserBiometricUnlockKey(userId = userId) != null
151+
152+
private fun isDeviceTrusted(
153+
userId: String,
154+
): Boolean = authDiskSource.getDeviceKey(userId = userId) != null
155+
156+
private fun getVaultUnlockType(
157+
userId: String,
158+
): VaultUnlockType = authDiskSource
159+
.getPinProtectedUserKey(userId = userId)
160+
?.let { VaultUnlockType.PIN }
161+
?: VaultUnlockType.MASTER_PASSWORD
162+
}

app/src/main/kotlin/com/x8bit/bitwarden/data/auth/repository/AuthRepository.kt

Lines changed: 2 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import com.bitwarden.network.model.TwoFactorDataModel
66
import com.x8bit.bitwarden.data.auth.datasource.disk.model.ForcePasswordResetReason
77
import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus
88
import com.x8bit.bitwarden.data.auth.manager.AuthRequestManager
9+
import com.x8bit.bitwarden.data.auth.manager.UserStateManager
910
import com.x8bit.bitwarden.data.auth.repository.model.AuthState
1011
import com.x8bit.bitwarden.data.auth.repository.model.BreachCountResult
1112
import com.x8bit.bitwarden.data.auth.repository.model.DeleteAccountResult
@@ -27,7 +28,6 @@ import com.x8bit.bitwarden.data.auth.repository.model.ResetPasswordResult
2728
import com.x8bit.bitwarden.data.auth.repository.model.SendVerificationEmailResult
2829
import com.x8bit.bitwarden.data.auth.repository.model.SetPasswordResult
2930
import com.x8bit.bitwarden.data.auth.repository.model.SwitchAccountResult
30-
import com.x8bit.bitwarden.data.auth.repository.model.UserState
3131
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePasswordResult
3232
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePinResult
3333
import com.x8bit.bitwarden.data.auth.repository.model.VerifiedOrganizationDomainSsoDetailsResult
@@ -44,17 +44,12 @@ import kotlinx.coroutines.flow.StateFlow
4444
* Provides an API for observing an modifying authentication state.
4545
*/
4646
@Suppress("TooManyFunctions")
47-
interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
47+
interface AuthRepository : AuthenticatorProvider, AuthRequestManager, UserStateManager {
4848
/**
4949
* Models the current auth state.
5050
*/
5151
val authStateFlow: StateFlow<AuthState>
5252

53-
/**
54-
* Emits updates for changes to the [UserState].
55-
*/
56-
val userStateFlow: StateFlow<UserState?>
57-
5853
/**
5954
* Flow of the current [DuoCallbackTokenResult]. Subscribers should listen to the flow
6055
* in order to receive updates whenever [setDuoCallbackTokenResult] is called.
@@ -110,15 +105,6 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
110105
*/
111106
var shouldTrustDevice: Boolean
112107

113-
/**
114-
* Tracks whether there is an additional account that is pending login/registration in order to
115-
* have multiple accounts available.
116-
*
117-
* This allows a direct view into and modification of [UserState.hasPendingAccountAddition].
118-
* Note that this call has no effect when there is no [UserState] information available.
119-
*/
120-
var hasPendingAccountAddition: Boolean
121-
122108
/**
123109
* Return the cached password policies for the current user.
124110
*/
@@ -140,11 +126,6 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
140126
*/
141127
val showWelcomeCarousel: Boolean
142128

143-
/**
144-
* Clears the pending deletion state that occurs when the an account is successfully deleted.
145-
*/
146-
fun clearPendingAccountDeletion()
147-
148129
/**
149130
* Attempt to delete the current account using the [masterPassword] and log them out
150131
* upon success.

0 commit comments

Comments
 (0)