mirror of
https://github.com/bitwarden/android.git
synced 2026-06-08 16:17:05 -05:00
BIT-1082: Implement vault unlock functionality (#263)
This commit is contained in:
committed by
Álison Fernandes
parent
17cd6c3cb0
commit
95b38605ee
@@ -1,6 +1,7 @@
|
||||
package com.x8bit.bitwarden.data.auth.repository
|
||||
|
||||
import com.x8bit.bitwarden.data.auth.datasource.sdk.model.PasswordStrength
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.AuthState
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.LoginResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.RegisterResult
|
||||
@@ -17,6 +18,11 @@ interface AuthRepository {
|
||||
*/
|
||||
val authStateFlow: StateFlow<AuthState>
|
||||
|
||||
/**
|
||||
* Emits updates for changes to the [UserState].
|
||||
*/
|
||||
val userStateFlow: StateFlow<UserState?>
|
||||
|
||||
/**
|
||||
* Flow of the current [CaptchaCallbackTokenResult]. Subscribers should listen to the flow
|
||||
* in order to receive updates whenever [setCaptchaCallbackTokenResult] is called.
|
||||
|
||||
@@ -16,6 +16,7 @@ import com.x8bit.bitwarden.data.auth.datasource.sdk.util.toKdfTypeJson
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.AuthState
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.LoginResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.RegisterResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.CaptchaCallbackTokenResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.toSdkParams
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.toUserState
|
||||
@@ -33,6 +34,7 @@ import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import javax.inject.Singleton
|
||||
@@ -75,6 +77,22 @@ class AuthRepositoryImpl constructor(
|
||||
initialValue = AuthState.Uninitialized,
|
||||
)
|
||||
|
||||
override val userStateFlow: StateFlow<UserState?> = combine(
|
||||
authDiskSource.userStateFlow,
|
||||
vaultRepository.vaultStateFlow,
|
||||
) { userStateJson, vaultState ->
|
||||
userStateJson?.toUserState(vaultState = vaultState)
|
||||
}
|
||||
.stateIn(
|
||||
scope = scope,
|
||||
started = SharingStarted.Eagerly,
|
||||
initialValue = authDiskSource
|
||||
.userState
|
||||
?.toUserState(
|
||||
vaultState = vaultRepository.vaultStateFlow.value,
|
||||
),
|
||||
)
|
||||
|
||||
private val mutableCaptchaTokenFlow =
|
||||
MutableSharedFlow<CaptchaCallbackTokenResult>(extraBufferCapacity = Int.MAX_VALUE)
|
||||
override val captchaTokenResultFlow: Flow<CaptchaCallbackTokenResult> =
|
||||
@@ -132,6 +150,7 @@ class AuthRepositoryImpl constructor(
|
||||
.environmentUrlData,
|
||||
)
|
||||
vaultRepository.unlockVault(
|
||||
userId = userStateJson.activeUserId,
|
||||
email = userStateJson.activeAccount.profile.email,
|
||||
kdf = userStateJson.activeAccount.profile.toSdkParams(),
|
||||
userKey = loginResponse.key,
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.x8bit.bitwarden.data.auth.repository.model
|
||||
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState.Account
|
||||
|
||||
/**
|
||||
* Represents the overall "user state" of the current active user as well as any users that may be
|
||||
* switched to.
|
||||
*
|
||||
* @property activeUserId The ID of the current active user.
|
||||
* @property accounts A mapping between user IDs and the [Account] information associated with
|
||||
* that user.
|
||||
*/
|
||||
data class UserState(
|
||||
val activeUserId: String,
|
||||
val accounts: List<Account>,
|
||||
) {
|
||||
init {
|
||||
require(accounts.any { it.userId == activeUserId })
|
||||
}
|
||||
|
||||
/**
|
||||
* The [Account] associated with the current [activeUserId].
|
||||
*/
|
||||
val activeAccount: Account
|
||||
get() = accounts.first { it.userId == activeUserId }
|
||||
|
||||
/**
|
||||
* Basic account information about a given user.
|
||||
*
|
||||
* @property userId The ID of the user.
|
||||
* @property email The user's email address.
|
||||
* @property name The user's name (if applicable).
|
||||
* @property avatarColorHex Hex color value for a user's avatar in the "#AARRGGBB" format.
|
||||
* @property isVaultUnlocked Whether or not the user's vault is currently unlocked.
|
||||
*/
|
||||
data class Account(
|
||||
val userId: String,
|
||||
val name: String?,
|
||||
val email: String,
|
||||
val avatarColorHex: String,
|
||||
val isVaultUnlocked: Boolean,
|
||||
)
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
package com.x8bit.bitwarden.data.auth.repository.util
|
||||
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultState
|
||||
|
||||
/**
|
||||
* Updates the given [UserStateJson] with the data from the [syncResponse] to return a new
|
||||
@@ -31,3 +33,27 @@ fun UserStateJson.toUpdatedUserStateJson(
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the given [UserStateJson] to a [UserState] using the given [vaultState].
|
||||
*/
|
||||
fun UserStateJson.toUserState(
|
||||
vaultState: VaultState,
|
||||
): UserState =
|
||||
UserState(
|
||||
activeUserId = this.activeUserId,
|
||||
accounts = this
|
||||
.accounts
|
||||
.values
|
||||
.map { accountJson ->
|
||||
val userId = accountJson.profile.userId
|
||||
UserState.Account(
|
||||
userId = accountJson.profile.userId,
|
||||
name = accountJson.profile.name,
|
||||
email = accountJson.profile.email,
|
||||
// TODO Calculate default color (BIT-1191)
|
||||
avatarColorHex = accountJson.profile.avatarColorHex ?: "#00aaaa",
|
||||
isVaultUnlocked = userId in vaultState.unlockedVaultUserIds,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
@@ -6,6 +6,7 @@ import com.bitwarden.core.Kdf
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.DataState
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.SendData
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultData
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultState
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
@@ -19,6 +20,11 @@ interface VaultRepository {
|
||||
*/
|
||||
val vaultDataStateFlow: StateFlow<DataState<VaultData>>
|
||||
|
||||
/**
|
||||
* Flow that represents the current vault state.
|
||||
*/
|
||||
val vaultStateFlow: StateFlow<VaultState>
|
||||
|
||||
/**
|
||||
* Flow that represents the current send data.
|
||||
*/
|
||||
@@ -56,6 +62,7 @@ interface VaultRepository {
|
||||
*/
|
||||
@Suppress("LongParameterList")
|
||||
suspend fun unlockVault(
|
||||
userId: String,
|
||||
masterPassword: String,
|
||||
email: String,
|
||||
kdf: Kdf,
|
||||
|
||||
@@ -17,6 +17,7 @@ import com.x8bit.bitwarden.data.vault.datasource.network.service.SyncService
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.SendData
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultData
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultState
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkCipherList
|
||||
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkFolderList
|
||||
@@ -39,6 +40,7 @@ import kotlinx.coroutines.launch
|
||||
/**
|
||||
* Default implementation of [VaultRepository].
|
||||
*/
|
||||
@Suppress("TooManyFunctions")
|
||||
class VaultRepositoryImpl constructor(
|
||||
private val syncService: SyncService,
|
||||
private val vaultSdkSource: VaultSdkSource,
|
||||
@@ -55,12 +57,18 @@ class VaultRepositoryImpl constructor(
|
||||
private val vaultDataMutableStateFlow =
|
||||
MutableStateFlow<DataState<VaultData>>(DataState.Loading)
|
||||
|
||||
private val vaultMutableStateFlow =
|
||||
MutableStateFlow(VaultState(unlockedVaultUserIds = emptySet()))
|
||||
|
||||
private val sendDataMutableStateFlow =
|
||||
MutableStateFlow<DataState<SendData>>(DataState.Loading)
|
||||
|
||||
override val vaultDataStateFlow: StateFlow<DataState<VaultData>>
|
||||
get() = vaultDataMutableStateFlow.asStateFlow()
|
||||
|
||||
override val vaultStateFlow: StateFlow<VaultState>
|
||||
get() = vaultMutableStateFlow.asStateFlow()
|
||||
|
||||
override val sendDataStateFlow: StateFlow<DataState<SendData>>
|
||||
get() = sendDataMutableStateFlow.asStateFlow()
|
||||
|
||||
@@ -157,6 +165,7 @@ class VaultRepositoryImpl constructor(
|
||||
val privateKey = authDiskSource.getPrivateKey(userId = userState.activeUserId)
|
||||
?: return VaultUnlockResult.InvalidStateError
|
||||
return unlockVault(
|
||||
userId = userState.activeUserId,
|
||||
masterPassword = masterPassword,
|
||||
email = userState.activeAccount.profile.email,
|
||||
kdf = userState.activeAccount.profile.toSdkParams(),
|
||||
@@ -173,6 +182,7 @@ class VaultRepositoryImpl constructor(
|
||||
}
|
||||
|
||||
override suspend fun unlockVault(
|
||||
userId: String,
|
||||
masterPassword: String,
|
||||
email: String,
|
||||
kdf: Kdf,
|
||||
@@ -196,13 +206,31 @@ class VaultRepositoryImpl constructor(
|
||||
)
|
||||
.fold(
|
||||
onFailure = { VaultUnlockResult.GenericError },
|
||||
onSuccess = { it.toVaultUnlockResult() },
|
||||
onSuccess = { initializeCryptoResult ->
|
||||
initializeCryptoResult
|
||||
.toVaultUnlockResult()
|
||||
.also {
|
||||
if (it is VaultUnlockResult.Success) {
|
||||
setVaultToUnlocked(userId = userId)
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
.onCompletion { willSyncAfterUnlock = false }
|
||||
.first()
|
||||
|
||||
// TODO: This is temporary. Eventually this needs to be based on the presence of various
|
||||
// user keys but this will likely require SDK updates to support this (BIT-1190).
|
||||
private fun setVaultToUnlocked(userId: String) {
|
||||
vaultMutableStateFlow.update {
|
||||
it.copy(
|
||||
unlockedVaultUserIds = it.unlockedVaultUserIds + userId,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun storeUserKeyAndPrivateKey(
|
||||
userKey: String?,
|
||||
privateKey: String?,
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.x8bit.bitwarden.data.vault.repository.model
|
||||
|
||||
/**
|
||||
* General description of the vault across multiple users.
|
||||
*
|
||||
* @property unlockedVaultUserIds The user IDs for all users that currently have unlocked vaults.
|
||||
*/
|
||||
data class VaultState(
|
||||
val unlockedVaultUserIds: Set<String>,
|
||||
)
|
||||
@@ -6,7 +6,7 @@ import androidx.navigation.NavOptions
|
||||
import androidx.navigation.compose.composable
|
||||
import com.x8bit.bitwarden.ui.platform.theme.TransitionProviders
|
||||
|
||||
private const val VAULT_UNLOCK: String = "vault_unlock"
|
||||
const val VAULT_UNLOCK_ROUTE: String = "vault_unlock"
|
||||
|
||||
/**
|
||||
* Navigate to the Vault Unlock screen.
|
||||
@@ -14,7 +14,7 @@ private const val VAULT_UNLOCK: String = "vault_unlock"
|
||||
fun NavController.navigateToVaultUnlock(
|
||||
navOptions: NavOptions? = null,
|
||||
) {
|
||||
navigate(VAULT_UNLOCK, navOptions)
|
||||
navigate(VAULT_UNLOCK_ROUTE, navOptions)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -22,7 +22,7 @@ fun NavController.navigateToVaultUnlock(
|
||||
*/
|
||||
fun NavGraphBuilder.vaultUnlockDestinations() {
|
||||
composable(
|
||||
route = VAULT_UNLOCK,
|
||||
route = VAULT_UNLOCK_ROUTE,
|
||||
enterTransition = TransitionProviders.Enter.slideUp,
|
||||
exitTransition = TransitionProviders.Exit.slideDown,
|
||||
popEnterTransition = TransitionProviders.Enter.slideUp,
|
||||
|
||||
@@ -5,6 +5,7 @@ import androidx.compose.ui.graphics.Color
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult
|
||||
@@ -14,6 +15,9 @@ import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.hexToColor
|
||||
import com.x8bit.bitwarden.ui.platform.components.model.AccountSummary
|
||||
import com.x8bit.bitwarden.ui.platform.util.labelOrBaseUrlHost
|
||||
import com.x8bit.bitwarden.ui.vault.feature.vault.util.initials
|
||||
import com.x8bit.bitwarden.ui.vault.feature.vault.util.toAccountSummaries
|
||||
import com.x8bit.bitwarden.ui.vault.feature.vault.util.toActiveAccountSummary
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
@@ -30,18 +34,24 @@ private const val KEY_STATE = "state"
|
||||
@HiltViewModel
|
||||
class VaultUnlockViewModel @Inject constructor(
|
||||
private val savedStateHandle: SavedStateHandle,
|
||||
private val authRepository: AuthRepository,
|
||||
private val vaultRepo: VaultRepository,
|
||||
environmentRepo: EnvironmentRepository,
|
||||
) : BaseViewModel<VaultUnlockState, VaultUnlockEvent, VaultUnlockAction>(
|
||||
initialState = savedStateHandle[KEY_STATE] ?: VaultUnlockState(
|
||||
accountSummaries = emptyList(),
|
||||
avatarColorString = "0000FF",
|
||||
initials = "BW",
|
||||
email = "bit@bitwarden.com",
|
||||
dialog = null,
|
||||
environmentUrl = environmentRepo.environment.labelOrBaseUrlHost,
|
||||
passwordInput = "",
|
||||
),
|
||||
initialState = savedStateHandle[KEY_STATE] ?: run {
|
||||
val userState = requireNotNull(authRepository.userStateFlow.value)
|
||||
val accountSummaries = userState.toAccountSummaries()
|
||||
val activeAccountSummary = userState.toActiveAccountSummary()
|
||||
VaultUnlockState(
|
||||
accountSummaries = accountSummaries,
|
||||
avatarColorString = activeAccountSummary.avatarColorHex,
|
||||
initials = activeAccountSummary.initials,
|
||||
email = activeAccountSummary.email,
|
||||
dialog = null,
|
||||
environmentUrl = environmentRepo.environment.labelOrBaseUrlHost,
|
||||
passwordInput = "",
|
||||
)
|
||||
},
|
||||
) {
|
||||
init {
|
||||
stateFlow
|
||||
|
||||
@@ -13,6 +13,9 @@ import androidx.navigation.navOptions
|
||||
import com.x8bit.bitwarden.ui.auth.feature.auth.AUTH_GRAPH_ROUTE
|
||||
import com.x8bit.bitwarden.ui.auth.feature.auth.authGraph
|
||||
import com.x8bit.bitwarden.ui.auth.feature.auth.navigateToAuthGraph
|
||||
import com.x8bit.bitwarden.ui.auth.feature.vaultunlock.VAULT_UNLOCK_ROUTE
|
||||
import com.x8bit.bitwarden.ui.auth.feature.vaultunlock.navigateToVaultUnlock
|
||||
import com.x8bit.bitwarden.ui.auth.feature.vaultunlock.vaultUnlockDestinations
|
||||
import com.x8bit.bitwarden.ui.platform.feature.splash.SPLASH_ROUTE
|
||||
import com.x8bit.bitwarden.ui.platform.feature.splash.navigateToSplash
|
||||
import com.x8bit.bitwarden.ui.platform.feature.splash.splashDestination
|
||||
@@ -47,12 +50,14 @@ fun RootNavScreen(
|
||||
) {
|
||||
splashDestination()
|
||||
authGraph(navController)
|
||||
vaultUnlockDestinations()
|
||||
vaultUnlockedGraph(navController)
|
||||
}
|
||||
|
||||
val targetRoute = when (state) {
|
||||
RootNavState.Auth -> AUTH_GRAPH_ROUTE
|
||||
RootNavState.Splash -> SPLASH_ROUTE
|
||||
RootNavState.VaultLocked -> VAULT_UNLOCK_ROUTE
|
||||
RootNavState.VaultUnlocked -> VAULT_UNLOCKED_GRAPH_ROUTE
|
||||
}
|
||||
val currentRoute = navController.currentDestination?.rootLevelRoute()
|
||||
@@ -77,6 +82,7 @@ fun RootNavScreen(
|
||||
when (state) {
|
||||
RootNavState.Auth -> navController.navigateToAuthGraph(rootNavOptions)
|
||||
RootNavState.Splash -> navController.navigateToSplash(rootNavOptions)
|
||||
RootNavState.VaultLocked -> navController.navigateToVaultUnlock(rootNavOptions)
|
||||
RootNavState.VaultUnlocked -> navController.navigateToVaultUnlockedGraph(rootNavOptions)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
package com.x8bit.bitwarden.ui.platform.feature.rootnav
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.AuthState
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
@@ -20,44 +19,33 @@ private const val KEY_NAV_DESTINATION = "nav_state"
|
||||
*/
|
||||
@HiltViewModel
|
||||
class RootNavViewModel @Inject constructor(
|
||||
private val authRepository: AuthRepository,
|
||||
private val savedStateHandle: SavedStateHandle,
|
||||
authRepository: AuthRepository,
|
||||
) : BaseViewModel<RootNavState, Unit, RootNavAction>(
|
||||
initialState = RootNavState.Splash,
|
||||
) {
|
||||
|
||||
private var savedRootNavState: RootNavState?
|
||||
get() = savedStateHandle[KEY_NAV_DESTINATION]
|
||||
set(value) {
|
||||
savedStateHandle[KEY_NAV_DESTINATION] = value
|
||||
}
|
||||
|
||||
init {
|
||||
savedRootNavState?.let { savedState: RootNavState ->
|
||||
mutableStateFlow.update { savedState }
|
||||
}
|
||||
// Every time the nav state changes, update saved state handle:
|
||||
stateFlow
|
||||
.onEach { savedRootNavState = it }
|
||||
.launchIn(viewModelScope)
|
||||
authRepository
|
||||
.authStateFlow
|
||||
.onEach { trySendAction(RootNavAction.AuthStateUpdated(it)) }
|
||||
.userStateFlow
|
||||
.onEach { sendAction(RootNavAction.Internal.UserStateUpdateReceive(it)) }
|
||||
.launchIn(viewModelScope)
|
||||
}
|
||||
|
||||
override fun handleAction(action: RootNavAction) {
|
||||
when (action) {
|
||||
is RootNavAction.AuthStateUpdated -> handleAuthStateUpdated(action)
|
||||
is RootNavAction.Internal.UserStateUpdateReceive -> handleUserStateUpdateReceive(action)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleAuthStateUpdated(action: RootNavAction.AuthStateUpdated) {
|
||||
when (action.newState) {
|
||||
is AuthState.Authenticated -> mutableStateFlow.update { RootNavState.VaultUnlocked }
|
||||
is AuthState.Unauthenticated -> mutableStateFlow.update { RootNavState.Auth }
|
||||
is AuthState.Uninitialized -> mutableStateFlow.update { RootNavState.Splash }
|
||||
private fun handleUserStateUpdateReceive(
|
||||
action: RootNavAction.Internal.UserStateUpdateReceive,
|
||||
) {
|
||||
val userState = action.userState
|
||||
val updatedRootNavState = when {
|
||||
userState == null -> RootNavState.Auth
|
||||
userState.activeAccount.isVaultUnlocked -> RootNavState.VaultUnlocked
|
||||
else -> RootNavState.VaultLocked
|
||||
}
|
||||
mutableStateFlow.update { updatedRootNavState }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,6 +65,12 @@ sealed class RootNavState : Parcelable {
|
||||
@Parcelize
|
||||
data object Splash : RootNavState()
|
||||
|
||||
/**
|
||||
* App should show vault locked nav graph.
|
||||
*/
|
||||
@Parcelize
|
||||
data object VaultLocked : RootNavState()
|
||||
|
||||
/**
|
||||
* App should show vault unlocked nav graph.
|
||||
*/
|
||||
@@ -90,7 +84,13 @@ sealed class RootNavState : Parcelable {
|
||||
sealed class RootNavAction {
|
||||
|
||||
/**
|
||||
* Auth state in the repository layer changed.
|
||||
* Internal ViewModel actions.
|
||||
*/
|
||||
data class AuthStateUpdated(val newState: AuthState) : RootNavAction()
|
||||
sealed class Internal {
|
||||
|
||||
/**
|
||||
* User state in the repository layer changed.
|
||||
*/
|
||||
data class UserStateUpdateReceive(val userState: UserState?) : RootNavAction()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,9 +3,10 @@ package com.x8bit.bitwarden.ui.vault.feature.vault
|
||||
import android.os.Parcelable
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.DataState
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultData
|
||||
@@ -16,9 +17,10 @@ import com.x8bit.bitwarden.ui.platform.base.util.concat
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.hexToColor
|
||||
import com.x8bit.bitwarden.ui.platform.components.model.AccountSummary
|
||||
import com.x8bit.bitwarden.ui.vault.feature.vault.util.initials
|
||||
import com.x8bit.bitwarden.ui.vault.feature.vault.util.toAccountSummaries
|
||||
import com.x8bit.bitwarden.ui.vault.feature.vault.util.toActiveAccountSummary
|
||||
import com.x8bit.bitwarden.ui.vault.feature.vault.util.toViewState
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
@@ -27,29 +29,29 @@ import kotlinx.coroutines.launch
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val KEY_STATE = "state"
|
||||
|
||||
/**
|
||||
* Manages [VaultState], handles [VaultAction], and launches [VaultEvent] for the [VaultScreen].
|
||||
*/
|
||||
@Suppress("TooManyFunctions")
|
||||
@HiltViewModel
|
||||
class VaultViewModel @Inject constructor(
|
||||
private val savedStateHandle: SavedStateHandle,
|
||||
authRepository: AuthRepository,
|
||||
vaultRepository: VaultRepository,
|
||||
) : BaseViewModel<VaultState, VaultEvent, VaultAction>(
|
||||
initialState = savedStateHandle[KEY_STATE] ?: VaultState(
|
||||
initials = activeAccountSummary.initials,
|
||||
avatarColorString = activeAccountSummary.avatarColorHex,
|
||||
accountSummaries = accountSummaries,
|
||||
viewState = VaultState.ViewState.Loading,
|
||||
),
|
||||
initialState = run {
|
||||
val userState = requireNotNull(authRepository.userStateFlow.value)
|
||||
val accountSummaries = userState.toAccountSummaries()
|
||||
val activeAccountSummary = userState.toActiveAccountSummary()
|
||||
VaultState(
|
||||
initials = activeAccountSummary.initials,
|
||||
avatarColorString = activeAccountSummary.avatarColorHex,
|
||||
accountSummaries = accountSummaries,
|
||||
viewState = VaultState.ViewState.Loading,
|
||||
)
|
||||
},
|
||||
) {
|
||||
|
||||
init {
|
||||
stateFlow
|
||||
.onEach { savedStateHandle[KEY_STATE] = it }
|
||||
.launchIn(viewModelScope)
|
||||
vaultRepository
|
||||
.vaultDataStateFlow
|
||||
.onEach { sendAction(VaultAction.Internal.VaultDataReceive(vaultData = it)) }
|
||||
@@ -66,6 +68,13 @@ class VaultViewModel @Inject constructor(
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
authRepository
|
||||
.userStateFlow
|
||||
.onEach {
|
||||
sendAction(VaultAction.Internal.UserStateUpdateReceive(userState = it))
|
||||
}
|
||||
.launchIn(viewModelScope)
|
||||
}
|
||||
|
||||
override fun handleAction(action: VaultAction) {
|
||||
@@ -81,6 +90,7 @@ class VaultViewModel @Inject constructor(
|
||||
is VaultAction.SecureNoteGroupClick -> handleSecureNoteClick()
|
||||
is VaultAction.TrashClick -> handleTrashClick()
|
||||
is VaultAction.VaultItemClick -> handleVaultItemClick(action)
|
||||
is VaultAction.Internal.UserStateUpdateReceive -> handleUserStateUpdateReceive(action)
|
||||
is VaultAction.Internal.VaultDataReceive -> handleVaultDataReceive(action)
|
||||
}
|
||||
}
|
||||
@@ -144,6 +154,22 @@ class VaultViewModel @Inject constructor(
|
||||
sendEvent(VaultEvent.NavigateToVaultItem(action.vaultItem.id))
|
||||
}
|
||||
|
||||
private fun handleUserStateUpdateReceive(action: VaultAction.Internal.UserStateUpdateReceive) {
|
||||
// Leave the current data alone if there is no UserState; we are in the process of logging
|
||||
// out.
|
||||
val userState = action.userState ?: return
|
||||
|
||||
mutableStateFlow.update {
|
||||
val accountSummaries = userState.toAccountSummaries()
|
||||
val activeAccountSummary = userState.toActiveAccountSummary()
|
||||
it.copy(
|
||||
initials = activeAccountSummary.initials,
|
||||
avatarColorString = activeAccountSummary.avatarColorHex,
|
||||
accountSummaries = accountSummaries,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleVaultDataReceive(action: VaultAction.Internal.VaultDataReceive) {
|
||||
when (val vaultData = action.vaultData) {
|
||||
is DataState.Error -> vaultErrorReceive(vaultData = vaultData)
|
||||
@@ -182,34 +208,6 @@ class VaultViewModel @Inject constructor(
|
||||
//endregion VaultAction Handlers
|
||||
}
|
||||
|
||||
// TODO: Get data from repository (BIT-205)
|
||||
private val accountSummaries = persistentListOf(
|
||||
AccountSummary(
|
||||
userId = "lockedUserId",
|
||||
name = "Locked User",
|
||||
email = "locked@bitwarden.com",
|
||||
avatarColorHex = "#00aaaa",
|
||||
status = AccountSummary.Status.LOCKED,
|
||||
),
|
||||
AccountSummary(
|
||||
userId = "activeUserId",
|
||||
name = "Active User",
|
||||
email = "active@bitwarden.com",
|
||||
avatarColorHex = "#aa00aa",
|
||||
status = AccountSummary.Status.ACTIVE,
|
||||
),
|
||||
AccountSummary(
|
||||
userId = "unlockedUserId",
|
||||
name = "Unlocked User",
|
||||
email = "unlocked@bitwarden.com",
|
||||
avatarColorHex = "#aaaa00",
|
||||
status = AccountSummary.Status.UNLOCKED,
|
||||
),
|
||||
)
|
||||
|
||||
private val activeAccountSummary = accountSummaries
|
||||
.first { it.status == AccountSummary.Status.ACTIVE }
|
||||
|
||||
/**
|
||||
* Represents the overall state for the [VaultScreen].
|
||||
*
|
||||
@@ -541,6 +539,13 @@ sealed class VaultAction {
|
||||
*/
|
||||
sealed class Internal : VaultAction() {
|
||||
|
||||
/**
|
||||
* Indicates a change in user state has been received.
|
||||
*/
|
||||
data class UserStateUpdateReceive(
|
||||
val userState: UserState?,
|
||||
) : Internal()
|
||||
|
||||
/**
|
||||
* Indicates a vault data was received.
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.x8bit.bitwarden.ui.vault.feature.vault.util
|
||||
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
||||
import com.x8bit.bitwarden.ui.platform.components.model.AccountSummary
|
||||
|
||||
/**
|
||||
* Converts the given [UserState] to a list of [AccountSummary].
|
||||
*/
|
||||
fun UserState.toAccountSummaries(): List<AccountSummary> =
|
||||
accounts.map { account ->
|
||||
account.toAccountSummary(
|
||||
isActive = this.activeUserId == account.userId,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the given [UserState] to an [AccountSummary] with a [AccountSummary.status] of
|
||||
* [AccountSummary.Status.ACTIVE].
|
||||
*/
|
||||
fun UserState.toActiveAccountSummary(): AccountSummary =
|
||||
this
|
||||
.activeAccount
|
||||
.toAccountSummary(isActive = true)
|
||||
|
||||
/**
|
||||
* Converts the given [UserState.Account] to an [AccountSummary] with the correct
|
||||
* [AccountSummary.Status]. The status will take into account whether or not the given account
|
||||
* [isActive].
|
||||
*/
|
||||
fun UserState.Account.toAccountSummary(
|
||||
isActive: Boolean,
|
||||
): AccountSummary =
|
||||
AccountSummary(
|
||||
userId = this.userId,
|
||||
name = this.name,
|
||||
email = this.email,
|
||||
avatarColorHex = this.avatarColorHex,
|
||||
status = when {
|
||||
isActive -> AccountSummary.Status.ACTIVE
|
||||
this.isVaultUnlocked -> AccountSummary.Status.UNLOCKED
|
||||
else -> AccountSummary.Status.LOCKED
|
||||
},
|
||||
)
|
||||
Reference in New Issue
Block a user