mirror of
https://github.com/bitwarden/android.git
synced 2025-03-15 10:48:47 +03:00
Ensure a SpecialCircumstance is scoped to a single overall graph (#712)
This commit is contained in:
parent
2f918650a1
commit
be7ccd3195
18 changed files with 252 additions and 134 deletions
|
@ -3,8 +3,8 @@ package com.x8bit.bitwarden
|
|||
import android.content.Intent
|
||||
import android.os.Parcelable
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
||||
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
|
||||
import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.model.AppTheme
|
||||
|
@ -21,7 +21,7 @@ import javax.inject.Inject
|
|||
*/
|
||||
@HiltViewModel
|
||||
class MainViewModel @Inject constructor(
|
||||
private val authRepository: AuthRepository,
|
||||
private val specialCircumstanceManager: SpecialCircumstanceManager,
|
||||
private val intentManager: IntentManager,
|
||||
settingsRepository: SettingsRepository,
|
||||
) : BaseViewModel<MainState, Unit, MainAction>(
|
||||
|
@ -69,8 +69,8 @@ class MainViewModel @Inject constructor(
|
|||
val shareData = intentManager.getShareDataFromIntent(intent)
|
||||
when {
|
||||
shareData != null -> {
|
||||
authRepository.specialCircumstance =
|
||||
UserState.SpecialCircumstance.ShareNewSend(
|
||||
specialCircumstanceManager.specialCircumstance =
|
||||
SpecialCircumstance.ShareNewSend(
|
||||
data = shareData,
|
||||
// Allow users back into the already-running app when completing the
|
||||
// Send task when this is not the first intent.
|
||||
|
|
|
@ -39,15 +39,6 @@ interface AuthRepository : AuthenticatorProvider {
|
|||
*/
|
||||
var rememberedEmailAddress: String?
|
||||
|
||||
/**
|
||||
* Any special account circumstances that may be relevant (ex: pending multi-user account
|
||||
* additions).
|
||||
*
|
||||
* This allows a direct view into and modification of [UserState.specialCircumstance].
|
||||
* Note that this call has no effect when there is no [UserState] information available.
|
||||
*/
|
||||
var specialCircumstance: UserState.SpecialCircumstance?
|
||||
|
||||
/**
|
||||
* Tracks whether there is an additional account that is pending login/registration in order to
|
||||
* have multiple accounts available.
|
||||
|
|
|
@ -74,8 +74,6 @@ class AuthRepositoryImpl(
|
|||
private val elapsedRealtimeMillisProvider: () -> Long = { SystemClock.elapsedRealtime() },
|
||||
) : AuthRepository {
|
||||
private val mutableHasPendingAccountAdditionStateFlow = MutableStateFlow<Boolean>(false)
|
||||
private val mutableSpecialCircumstanceStateFlow =
|
||||
MutableStateFlow<UserState.SpecialCircumstance?>(null)
|
||||
|
||||
/**
|
||||
* A scope intended for use when simply collecting multiple flows in order to combine them. The
|
||||
|
@ -109,20 +107,17 @@ class AuthRepositoryImpl(
|
|||
authDiskSource.userOrganizationsListFlow,
|
||||
vaultRepository.vaultStateFlow,
|
||||
mutableHasPendingAccountAdditionStateFlow,
|
||||
mutableSpecialCircumstanceStateFlow,
|
||||
) {
|
||||
userStateJson,
|
||||
userOrganizationsList,
|
||||
vaultState,
|
||||
hasPendingAccountAddition,
|
||||
specialCircumstance,
|
||||
->
|
||||
userStateJson
|
||||
?.toUserState(
|
||||
vaultState = vaultState,
|
||||
userOrganizationsList = userOrganizationsList,
|
||||
hasPendingAccountAddition = hasPendingAccountAddition,
|
||||
specialCircumstance = specialCircumstance,
|
||||
vaultUnlockTypeProvider = ::getVaultUnlockType,
|
||||
)
|
||||
}
|
||||
|
@ -135,7 +130,6 @@ class AuthRepositoryImpl(
|
|||
vaultState = vaultRepository.vaultStateFlow.value,
|
||||
userOrganizationsList = authDiskSource.userOrganizationsList,
|
||||
hasPendingAccountAddition = mutableHasPendingAccountAdditionStateFlow.value,
|
||||
specialCircumstance = mutableSpecialCircumstanceStateFlow.value,
|
||||
vaultUnlockTypeProvider = ::getVaultUnlockType,
|
||||
),
|
||||
)
|
||||
|
@ -147,9 +141,6 @@ class AuthRepositoryImpl(
|
|||
|
||||
override var rememberedEmailAddress: String? by authDiskSource::rememberedEmailAddress
|
||||
|
||||
override var specialCircumstance: UserState.SpecialCircumstance?
|
||||
by mutableSpecialCircumstanceStateFlow::value
|
||||
|
||||
override var hasPendingAccountAddition: Boolean
|
||||
by mutableHasPendingAccountAdditionStateFlow::value
|
||||
|
||||
|
|
|
@ -2,7 +2,6 @@ package com.x8bit.bitwarden.data.auth.repository.model
|
|||
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState.Account
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.Environment
|
||||
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
|
||||
|
||||
/**
|
||||
* Represents the overall "user state" of the current active user as well as any users that may be
|
||||
|
@ -19,7 +18,6 @@ data class UserState(
|
|||
val activeUserId: String,
|
||||
val accounts: List<Account>,
|
||||
val hasPendingAccountAddition: Boolean = false,
|
||||
val specialCircumstance: SpecialCircumstance? = null,
|
||||
) {
|
||||
init {
|
||||
require(accounts.any { it.userId == activeUserId })
|
||||
|
@ -58,17 +56,4 @@ data class UserState(
|
|||
val organizations: List<Organization>,
|
||||
val vaultUnlockType: VaultUnlockType = VaultUnlockType.MASTER_PASSWORD,
|
||||
)
|
||||
|
||||
/**
|
||||
* Represents a special account-related circumstance.
|
||||
*/
|
||||
sealed class SpecialCircumstance {
|
||||
/**
|
||||
* The app was launched in order to create/share a new Send using the given [data].
|
||||
*/
|
||||
data class ShareNewSend(
|
||||
val data: IntentManager.ShareData,
|
||||
val shouldFinishWhenComplete: Boolean,
|
||||
) : SpecialCircumstance()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -48,7 +48,6 @@ fun UserStateJson.toUserState(
|
|||
vaultState: VaultState,
|
||||
userOrganizationsList: List<UserOrganizations>,
|
||||
hasPendingAccountAddition: Boolean,
|
||||
specialCircumstance: UserState.SpecialCircumstance?,
|
||||
vaultUnlockTypeProvider: (userId: String) -> VaultUnlockType,
|
||||
): UserState =
|
||||
UserState(
|
||||
|
@ -79,5 +78,4 @@ fun UserStateJson.toUserState(
|
|||
)
|
||||
},
|
||||
hasPendingAccountAddition = hasPendingAccountAddition,
|
||||
specialCircumstance = specialCircumstance,
|
||||
)
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
package com.x8bit.bitwarden.data.platform.manager
|
||||
|
||||
import com.x8bit.bitwarden.MainActivity
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
/**
|
||||
* Tracks any [SpecialCircumstance] that may be present.
|
||||
*
|
||||
* Note that this will be scoped to the current "retained Activity": if there are multiple tasks
|
||||
* that each have a [MainActivity], they can each have a separate [SpecialCircumstance] associated
|
||||
* with them.
|
||||
*/
|
||||
interface SpecialCircumstanceManager {
|
||||
/**
|
||||
* Gets the current [SpecialCircumstance] if any.
|
||||
*/
|
||||
var specialCircumstance: SpecialCircumstance?
|
||||
|
||||
/**
|
||||
* Emits updates that track changes to [specialCircumstance].
|
||||
*/
|
||||
val specialCircumstanceStateFlow: StateFlow<SpecialCircumstance?>
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
package com.x8bit.bitwarden.data.platform.manager
|
||||
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
/**
|
||||
* Primary implementation of [SpecialCircumstanceManager].
|
||||
*/
|
||||
class SpecialCircumstanceManagerImpl : SpecialCircumstanceManager {
|
||||
private val mutableSpecialCircumstanceFlow = MutableStateFlow<SpecialCircumstance?>(null)
|
||||
|
||||
override var specialCircumstance: SpecialCircumstance?
|
||||
get() = mutableSpecialCircumstanceFlow.value
|
||||
set(value) {
|
||||
mutableSpecialCircumstanceFlow.value = value
|
||||
}
|
||||
|
||||
override val specialCircumstanceStateFlow: StateFlow<SpecialCircumstance?>
|
||||
get() = mutableSpecialCircumstanceFlow
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
package com.x8bit.bitwarden.data.platform.manager.di
|
||||
|
||||
import com.x8bit.bitwarden.MainActivity
|
||||
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManagerImpl
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.components.ActivityRetainedComponent
|
||||
import dagger.hilt.android.scopes.ActivityRetainedScoped
|
||||
|
||||
/**
|
||||
* Provides managers in the platform package that must be scoped to a retained Activity. These are
|
||||
* for dependencies that must operate independently in different application tasks that contain
|
||||
* unique [MainActivity] instances.
|
||||
*/
|
||||
@Module
|
||||
@InstallIn(ActivityRetainedComponent::class)
|
||||
class ActivityPlatformManagerModule {
|
||||
|
||||
@Provides
|
||||
@ActivityRetainedScoped
|
||||
fun provideActivityScopedSpecialCircumstanceRepository(): SpecialCircumstanceManager =
|
||||
SpecialCircumstanceManagerImpl()
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
package com.x8bit.bitwarden.data.platform.manager.model
|
||||
|
||||
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
|
||||
|
||||
/**
|
||||
* Represents a special circumstance the app may be in. These circumstances could require some kind
|
||||
* of navigation that is counter to what otherwise may happen based on the state of the app.
|
||||
*/
|
||||
sealed class SpecialCircumstance {
|
||||
/**
|
||||
* The app was launched in order to create/share a new Send using the given [data].
|
||||
*/
|
||||
data class ShareNewSend(
|
||||
val data: IntentManager.ShareData,
|
||||
val shouldFinishWhenComplete: Boolean,
|
||||
) : SpecialCircumstance()
|
||||
}
|
|
@ -4,29 +4,40 @@ import android.os.Parcelable
|
|||
import androidx.lifecycle.viewModelScope
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
||||
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val KEY_NAV_DESTINATION = "nav_state"
|
||||
|
||||
/**
|
||||
* Manages root level navigation state of the application.
|
||||
*/
|
||||
@HiltViewModel
|
||||
class RootNavViewModel @Inject constructor(
|
||||
private val authRepository: AuthRepository,
|
||||
private val specialCircumstanceManager: SpecialCircumstanceManager,
|
||||
) : BaseViewModel<RootNavState, Unit, RootNavAction>(
|
||||
initialState = RootNavState.Splash,
|
||||
) {
|
||||
init {
|
||||
authRepository
|
||||
.userStateFlow
|
||||
.onEach { sendAction(RootNavAction.Internal.UserStateUpdateReceive(it)) }
|
||||
combine(
|
||||
authRepository
|
||||
.userStateFlow,
|
||||
specialCircumstanceManager
|
||||
.specialCircumstanceStateFlow,
|
||||
) { userState, specialCircumstance ->
|
||||
RootNavAction.Internal.UserStateUpdateReceive(
|
||||
userState = userState,
|
||||
specialCircumstance = specialCircumstance,
|
||||
)
|
||||
}
|
||||
.onEach(::handleAction)
|
||||
.launchIn(viewModelScope)
|
||||
}
|
||||
|
||||
|
@ -45,16 +56,15 @@ class RootNavViewModel @Inject constructor(
|
|||
action: RootNavAction.Internal.UserStateUpdateReceive,
|
||||
) {
|
||||
val userState = action.userState
|
||||
val specialCircumstance = action.specialCircumstance
|
||||
val updatedRootNavState = when {
|
||||
userState == null ||
|
||||
!userState.activeAccount.isLoggedIn ||
|
||||
userState.hasPendingAccountAddition -> RootNavState.Auth
|
||||
|
||||
userState.activeAccount.isVaultUnlocked -> {
|
||||
when (userState.specialCircumstance) {
|
||||
is UserState.SpecialCircumstance.ShareNewSend -> {
|
||||
RootNavState.VaultUnlockedForNewSend
|
||||
}
|
||||
when (specialCircumstance) {
|
||||
is SpecialCircumstance.ShareNewSend -> RootNavState.VaultUnlockedForNewSend
|
||||
|
||||
null,
|
||||
-> {
|
||||
|
@ -126,6 +136,9 @@ sealed class RootNavAction {
|
|||
/**
|
||||
* User state in the repository layer changed.
|
||||
*/
|
||||
data class UserStateUpdateReceive(val userState: UserState?) : RootNavAction()
|
||||
data class UserStateUpdateReceive(
|
||||
val userState: UserState?,
|
||||
val specialCircumstance: SpecialCircumstance?,
|
||||
) : RootNavAction()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import com.bitwarden.core.SendView
|
|||
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.manager.SpecialCircumstanceManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager
|
||||
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.DataState
|
||||
|
@ -53,7 +54,7 @@ private const val MAX_FILE_SIZE_BYTES: Long = 100 * 1024 * 1024
|
|||
/**
|
||||
* View model for the new send screen.
|
||||
*/
|
||||
@Suppress("TooManyFunctions")
|
||||
@Suppress("TooManyFunctions", "LongParameterList")
|
||||
@HiltViewModel
|
||||
class AddSendViewModel @Inject constructor(
|
||||
savedStateHandle: SavedStateHandle,
|
||||
|
@ -61,12 +62,13 @@ class AddSendViewModel @Inject constructor(
|
|||
private val clock: Clock,
|
||||
private val clipboardManager: BitwardenClipboardManager,
|
||||
private val environmentRepo: EnvironmentRepository,
|
||||
private val specialCircumstanceManager: SpecialCircumstanceManager,
|
||||
private val vaultRepo: VaultRepository,
|
||||
) : BaseViewModel<AddSendState, AddSendEvent, AddSendAction>(
|
||||
// We load the state from the savedStateHandle for testing purposes.
|
||||
initialState = savedStateHandle[KEY_STATE] ?: run {
|
||||
// Check to see if we are navigating here from an external source
|
||||
val specialCircumstance = authRepo.specialCircumstance
|
||||
val specialCircumstance = specialCircumstanceManager.specialCircumstance
|
||||
val shareSendType = specialCircumstance.toSendType()
|
||||
val sendAddType = AddSendArgs(savedStateHandle).sendAddType
|
||||
AddSendState(
|
||||
|
@ -596,7 +598,7 @@ class AddSendViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun navigateBack() {
|
||||
authRepo.specialCircumstance = null
|
||||
specialCircumstanceManager.specialCircumstance = null
|
||||
sendEvent(
|
||||
event = if (state.shouldFinishOnComplete) {
|
||||
AddSendEvent.ExitApp
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
package com.x8bit.bitwarden.ui.tools.feature.send.addsend.util
|
||||
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance
|
||||
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
|
||||
import com.x8bit.bitwarden.ui.tools.feature.send.addsend.AddSendState
|
||||
|
||||
/**
|
||||
* Determines the initial [AddSendState.ViewState.Content.SendType] based on the data in the
|
||||
* [UserState.SpecialCircumstance].
|
||||
* [SpecialCircumstance].
|
||||
*/
|
||||
fun UserState.SpecialCircumstance?.toSendType(): AddSendState.ViewState.Content.SendType? =
|
||||
fun SpecialCircumstance?.toSendType(): AddSendState.ViewState.Content.SendType? =
|
||||
when (this) {
|
||||
is UserState.SpecialCircumstance.ShareNewSend -> {
|
||||
is SpecialCircumstance.ShareNewSend -> {
|
||||
when (data) {
|
||||
is IntentManager.ShareData.FileSend -> AddSendState.ViewState.Content.SendType.File(
|
||||
uri = data.fileData.uri,
|
||||
|
@ -30,11 +30,11 @@ fun UserState.SpecialCircumstance?.toSendType(): AddSendState.ViewState.Content.
|
|||
}
|
||||
|
||||
/**
|
||||
* Determines the initial send name based on the data in the [UserState.SpecialCircumstance].
|
||||
* Determines the initial send name based on the data in the [SpecialCircumstance].
|
||||
*/
|
||||
fun UserState.SpecialCircumstance?.toSendName(): String? =
|
||||
fun SpecialCircumstance?.toSendName(): String? =
|
||||
when (this) {
|
||||
is UserState.SpecialCircumstance.ShareNewSend -> {
|
||||
is SpecialCircumstance.ShareNewSend -> {
|
||||
when (data) {
|
||||
is IntentManager.ShareData.FileSend -> data.fileData.fileName
|
||||
is IntentManager.ShareData.TextSend -> data.subject
|
||||
|
@ -45,11 +45,10 @@ fun UserState.SpecialCircumstance?.toSendName(): String? =
|
|||
}
|
||||
|
||||
/**
|
||||
* Determines if the [UserState.SpecialCircumstance] requires the app to be closed after completing
|
||||
* the send.
|
||||
* Determines if the [SpecialCircumstance] requires the app to be closed after completing the send.
|
||||
*/
|
||||
fun UserState.SpecialCircumstance?.shouldFinishOnComplete(): Boolean =
|
||||
fun SpecialCircumstance?.shouldFinishOnComplete(): Boolean =
|
||||
when (this) {
|
||||
is UserState.SpecialCircumstance.ShareNewSend -> shouldFinishWhenComplete
|
||||
is SpecialCircumstance.ShareNewSend -> shouldFinishWhenComplete
|
||||
else -> false
|
||||
}
|
||||
|
|
|
@ -4,15 +4,15 @@ import android.content.Intent
|
|||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.getCaptchaCallbackTokenResult
|
||||
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManagerImpl
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.Environment
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
|
||||
import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.model.AppTheme
|
||||
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
|
||||
import io.mockk.every
|
||||
import io.mockk.just
|
||||
import io.mockk.mockk
|
||||
import io.mockk.runs
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
|
@ -25,13 +25,12 @@ class MainViewModelTest : BaseViewModelTest() {
|
|||
val authRepository = mockk<AuthRepository> {
|
||||
every { userStateFlow } returns mutableUserStateFlow
|
||||
every { activeUserId } returns USER_ID
|
||||
every { specialCircumstance } returns null
|
||||
every { specialCircumstance = any() } just runs
|
||||
}
|
||||
private val settingsRepository = mockk<SettingsRepository> {
|
||||
every { appTheme } returns AppTheme.DEFAULT
|
||||
every { appThemeStateFlow } returns mutableAppThemeFlow
|
||||
}
|
||||
private val specialCircumstanceManager = SpecialCircumstanceManagerImpl()
|
||||
private val intentManager: IntentManager = mockk {
|
||||
every { getShareDataFromIntent(any()) } returns null
|
||||
}
|
||||
|
@ -78,12 +77,13 @@ class MainViewModelTest : BaseViewModelTest() {
|
|||
intent = mockIntent,
|
||||
),
|
||||
)
|
||||
verify {
|
||||
authRepository.specialCircumstance = UserState.SpecialCircumstance.ShareNewSend(
|
||||
assertEquals(
|
||||
SpecialCircumstance.ShareNewSend(
|
||||
data = shareData,
|
||||
shouldFinishWhenComplete = true,
|
||||
)
|
||||
}
|
||||
),
|
||||
specialCircumstanceManager.specialCircumstance,
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
|
@ -100,16 +100,17 @@ class MainViewModelTest : BaseViewModelTest() {
|
|||
intent = mockIntent,
|
||||
),
|
||||
)
|
||||
verify {
|
||||
authRepository.specialCircumstance = UserState.SpecialCircumstance.ShareNewSend(
|
||||
assertEquals(
|
||||
SpecialCircumstance.ShareNewSend(
|
||||
data = shareData,
|
||||
shouldFinishWhenComplete = false,
|
||||
)
|
||||
}
|
||||
),
|
||||
specialCircumstanceManager.specialCircumstance,
|
||||
)
|
||||
}
|
||||
|
||||
private fun createViewModel() = MainViewModel(
|
||||
authRepository = authRepository,
|
||||
specialCircumstanceManager = specialCircumstanceManager,
|
||||
settingsRepository = settingsRepository,
|
||||
intentManager = intentManager,
|
||||
)
|
||||
|
|
|
@ -35,7 +35,6 @@ import com.x8bit.bitwarden.data.auth.repository.model.PasswordStrengthResult
|
|||
import com.x8bit.bitwarden.data.auth.repository.model.RegisterResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.SwitchAccountResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserOrganizations
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.VaultUnlockType
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.CaptchaCallbackTokenResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.toOrganizations
|
||||
|
@ -219,7 +218,6 @@ class AuthRepositoryTest {
|
|||
vaultState = VAULT_STATE,
|
||||
userOrganizationsList = emptyList(),
|
||||
hasPendingAccountAddition = false,
|
||||
specialCircumstance = null,
|
||||
vaultUnlockTypeProvider = { VaultUnlockType.MASTER_PASSWORD },
|
||||
),
|
||||
repository.userStateFlow.value,
|
||||
|
@ -241,7 +239,6 @@ class AuthRepositoryTest {
|
|||
vaultState = VAULT_STATE,
|
||||
userOrganizationsList = emptyList(),
|
||||
hasPendingAccountAddition = false,
|
||||
specialCircumstance = null,
|
||||
vaultUnlockTypeProvider = { VaultUnlockType.PIN },
|
||||
),
|
||||
repository.userStateFlow.value,
|
||||
|
@ -257,7 +254,6 @@ class AuthRepositoryTest {
|
|||
vaultState = emptyVaultState,
|
||||
userOrganizationsList = emptyList(),
|
||||
hasPendingAccountAddition = false,
|
||||
specialCircumstance = null,
|
||||
vaultUnlockTypeProvider = { VaultUnlockType.PIN },
|
||||
),
|
||||
repository.userStateFlow.value,
|
||||
|
@ -282,7 +278,6 @@ class AuthRepositoryTest {
|
|||
vaultState = emptyVaultState,
|
||||
userOrganizationsList = USER_ORGANIZATIONS,
|
||||
hasPendingAccountAddition = false,
|
||||
specialCircumstance = null,
|
||||
vaultUnlockTypeProvider = { VaultUnlockType.MASTER_PASSWORD },
|
||||
),
|
||||
repository.userStateFlow.value,
|
||||
|
@ -304,35 +299,6 @@ class AuthRepositoryTest {
|
|||
assertNull(repository.rememberedEmailAddress)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `specialCircumstance update should trigger a change in UserState`() {
|
||||
// Populate the initial UserState
|
||||
assertNull(repository.specialCircumstance)
|
||||
val initialUserState = SINGLE_USER_STATE_1.toUserState(
|
||||
vaultState = VAULT_STATE,
|
||||
userOrganizationsList = emptyList(),
|
||||
hasPendingAccountAddition = false,
|
||||
specialCircumstance = null,
|
||||
vaultUnlockTypeProvider = { VaultUnlockType.MASTER_PASSWORD },
|
||||
)
|
||||
mutableVaultStateFlow.value = VAULT_STATE
|
||||
fakeAuthDiskSource.userState = SINGLE_USER_STATE_1
|
||||
assertEquals(
|
||||
initialUserState,
|
||||
repository.userStateFlow.value,
|
||||
)
|
||||
|
||||
val mockSpecialCircumstance: UserState.SpecialCircumstance = mockk()
|
||||
repository.specialCircumstance = mockSpecialCircumstance
|
||||
|
||||
assertEquals(
|
||||
initialUserState.copy(
|
||||
specialCircumstance = mockSpecialCircumstance,
|
||||
),
|
||||
repository.userStateFlow.value,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `delete account fails if not logged in`() = runTest {
|
||||
val masterPassword = "hello world"
|
||||
|
@ -591,7 +557,6 @@ class AuthRepositoryTest {
|
|||
SINGLE_USER_STATE_1,
|
||||
fakeAuthDiskSource.userState,
|
||||
)
|
||||
assertNull(repository.specialCircumstance)
|
||||
verify { settingsRepository.setDefaultsIfNecessary(userId = USER_ID_1) }
|
||||
verify { vaultRepository.clearUnlockedData() }
|
||||
}
|
||||
|
@ -1089,7 +1054,6 @@ class AuthRepositoryTest {
|
|||
vaultState = VAULT_STATE,
|
||||
userOrganizationsList = emptyList(),
|
||||
hasPendingAccountAddition = false,
|
||||
specialCircumstance = null,
|
||||
vaultUnlockTypeProvider = { VaultUnlockType.MASTER_PASSWORD },
|
||||
)
|
||||
fakeAuthDiskSource.userState = SINGLE_USER_STATE_1
|
||||
|
@ -1120,7 +1084,6 @@ class AuthRepositoryTest {
|
|||
vaultState = VAULT_STATE,
|
||||
userOrganizationsList = emptyList(),
|
||||
hasPendingAccountAddition = false,
|
||||
specialCircumstance = null,
|
||||
vaultUnlockTypeProvider = { VaultUnlockType.MASTER_PASSWORD },
|
||||
)
|
||||
fakeAuthDiskSource.userState = SINGLE_USER_STATE_1
|
||||
|
@ -1149,7 +1112,6 @@ class AuthRepositoryTest {
|
|||
vaultState = VAULT_STATE,
|
||||
userOrganizationsList = emptyList(),
|
||||
hasPendingAccountAddition = false,
|
||||
specialCircumstance = null,
|
||||
vaultUnlockTypeProvider = { VaultUnlockType.MASTER_PASSWORD },
|
||||
)
|
||||
fakeAuthDiskSource.userState = MULTI_USER_STATE
|
||||
|
|
|
@ -155,7 +155,6 @@ class UserStateJsonExtensionsTest {
|
|||
),
|
||||
),
|
||||
hasPendingAccountAddition = false,
|
||||
specialCircumstance = null,
|
||||
vaultUnlockTypeProvider = { VaultUnlockType.PIN },
|
||||
),
|
||||
)
|
||||
|
@ -187,7 +186,6 @@ class UserStateJsonExtensionsTest {
|
|||
),
|
||||
),
|
||||
hasPendingAccountAddition = true,
|
||||
specialCircumstance = MOCK_SPECIAL_CIRCUMSTANCE,
|
||||
),
|
||||
UserStateJson(
|
||||
activeUserId = "activeUserId",
|
||||
|
@ -227,11 +225,8 @@ class UserStateJsonExtensionsTest {
|
|||
),
|
||||
),
|
||||
hasPendingAccountAddition = true,
|
||||
specialCircumstance = MOCK_SPECIAL_CIRCUMSTANCE,
|
||||
vaultUnlockTypeProvider = { VaultUnlockType.MASTER_PASSWORD },
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private val MOCK_SPECIAL_CIRCUMSTANCE: UserState.SpecialCircumstance = mockk()
|
||||
|
|
|
@ -2,6 +2,8 @@ package com.x8bit.bitwarden.ui.platform.feature.rootnav
|
|||
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
||||
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManagerImpl
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.Environment
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
|
||||
import io.mockk.every
|
||||
|
@ -19,6 +21,7 @@ class RootNavViewModelTest : BaseViewModelTest() {
|
|||
every { userStateFlow } returns mutableUserStateFlow
|
||||
every { updateLastActiveTime() } just runs
|
||||
}
|
||||
private val specialCircumstanceManager = SpecialCircumstanceManagerImpl()
|
||||
|
||||
@Test
|
||||
fun `when there are no accounts the nav state should be Auth`() {
|
||||
|
@ -27,6 +30,57 @@ class RootNavViewModelTest : BaseViewModelTest() {
|
|||
assertEquals(RootNavState.Auth, viewModel.stateFlow.value)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `when the active user is not logged in the nav state should be Auth`() {
|
||||
mutableUserStateFlow.tryEmit(
|
||||
UserState(
|
||||
activeUserId = "activeUserId",
|
||||
accounts = listOf(
|
||||
UserState.Account(
|
||||
userId = "activeUserId",
|
||||
name = "name",
|
||||
email = "email",
|
||||
avatarColorHex = "avatarColorHex",
|
||||
environment = Environment.Us,
|
||||
isPremium = true,
|
||||
isLoggedIn = false,
|
||||
isVaultUnlocked = true,
|
||||
organizations = emptyList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
val viewModel = createViewModel()
|
||||
assertEquals(RootNavState.Auth, viewModel.stateFlow.value)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `when the active user but there are pending account additions the nav state should be Auth`() {
|
||||
mutableUserStateFlow.tryEmit(
|
||||
UserState(
|
||||
activeUserId = "activeUserId",
|
||||
accounts = listOf(
|
||||
UserState.Account(
|
||||
userId = "activeUserId",
|
||||
name = "name",
|
||||
email = "email",
|
||||
avatarColorHex = "avatarColorHex",
|
||||
environment = Environment.Us,
|
||||
isPremium = true,
|
||||
isLoggedIn = true,
|
||||
isVaultUnlocked = true,
|
||||
organizations = emptyList(),
|
||||
),
|
||||
),
|
||||
hasPendingAccountAddition = true,
|
||||
),
|
||||
)
|
||||
val viewModel = createViewModel()
|
||||
assertEquals(RootNavState.Auth, viewModel.stateFlow.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when the active user has an unlocked vault the nav state should be VaultUnlocked`() {
|
||||
mutableUserStateFlow.tryEmit(
|
||||
|
@ -54,6 +108,39 @@ class RootNavViewModelTest : BaseViewModelTest() {
|
|||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `when the active user has an unlocked vault but the is a ShareNewSend special circumstance the nav state should be VaultUnlockedForNewSend`() {
|
||||
specialCircumstanceManager.specialCircumstance =
|
||||
SpecialCircumstance.ShareNewSend(
|
||||
data = mockk(),
|
||||
shouldFinishWhenComplete = true,
|
||||
)
|
||||
mutableUserStateFlow.tryEmit(
|
||||
UserState(
|
||||
activeUserId = "activeUserId",
|
||||
accounts = listOf(
|
||||
UserState.Account(
|
||||
userId = "activeUserId",
|
||||
name = "name",
|
||||
email = "email",
|
||||
avatarColorHex = "avatarColorHex",
|
||||
environment = Environment.Us,
|
||||
isPremium = true,
|
||||
isLoggedIn = true,
|
||||
isVaultUnlocked = true,
|
||||
organizations = emptyList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
val viewModel = createViewModel()
|
||||
assertEquals(
|
||||
RootNavState.VaultUnlockedForNewSend,
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when the active user has a locked vault the nav state should be VaultLocked`() {
|
||||
mutableUserStateFlow.tryEmit(
|
||||
|
@ -88,5 +175,6 @@ class RootNavViewModelTest : BaseViewModelTest() {
|
|||
private fun createViewModel(): RootNavViewModel =
|
||||
RootNavViewModel(
|
||||
authRepository = authRepository,
|
||||
specialCircumstanceManager = specialCircumstanceManager,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import com.bitwarden.core.SendView
|
|||
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.manager.SpecialCircumstanceManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager
|
||||
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.DataState
|
||||
|
@ -56,13 +57,15 @@ class AddSendViewModelTest : BaseViewModelTest() {
|
|||
}
|
||||
private val mutableUserStateFlow = MutableStateFlow<UserState?>(DEFAULT_USER_STATE)
|
||||
private val authRepository: AuthRepository = mockk {
|
||||
every { specialCircumstance } returns null
|
||||
every { specialCircumstance = null } just runs
|
||||
every { userStateFlow } returns mutableUserStateFlow
|
||||
}
|
||||
private val environmentRepository: EnvironmentRepository = mockk {
|
||||
every { environment } returns Environment.Us
|
||||
}
|
||||
private val specialCircumstanceManager: SpecialCircumstanceManager = mockk {
|
||||
every { specialCircumstance } returns null
|
||||
every { specialCircumstance = any() } just runs
|
||||
}
|
||||
private val mutableSendDataStateFlow = MutableStateFlow<DataState<SendView>>(DataState.Loading)
|
||||
private val vaultRepository: VaultRepository = mockk {
|
||||
every { getSendStateFlow(any()) } returns mutableSendDataStateFlow
|
||||
|
@ -170,7 +173,7 @@ class AddSendViewModelTest : BaseViewModelTest() {
|
|||
assertEquals(initialState, viewModel.stateFlow.value)
|
||||
coVerify(exactly = 1) {
|
||||
vaultRepository.createSend(sendView = mockSendView, fileUri = null)
|
||||
authRepository.specialCircumstance = null
|
||||
specialCircumstanceManager.specialCircumstance = null
|
||||
clipboardManager.setText(sendUrl)
|
||||
}
|
||||
}
|
||||
|
@ -205,7 +208,7 @@ class AddSendViewModelTest : BaseViewModelTest() {
|
|||
assertEquals(initialState, viewModel.stateFlow.value)
|
||||
coVerify(exactly = 1) {
|
||||
vaultRepository.createSend(sendView = mockSendView, fileUri = null)
|
||||
authRepository.specialCircumstance = null
|
||||
specialCircumstanceManager.specialCircumstance = null
|
||||
clipboardManager.setText(sendUrl)
|
||||
}
|
||||
}
|
||||
|
@ -931,6 +934,7 @@ class AddSendViewModelTest : BaseViewModelTest() {
|
|||
private fun createViewModel(
|
||||
state: AddSendState? = null,
|
||||
addSendType: AddSendType = AddSendType.AddItem,
|
||||
activityToken: String? = null,
|
||||
): AddSendViewModel = AddSendViewModel(
|
||||
savedStateHandle = SavedStateHandle().apply {
|
||||
set("state", state?.copy(addSendType = addSendType))
|
||||
|
@ -942,9 +946,11 @@ class AddSendViewModelTest : BaseViewModelTest() {
|
|||
},
|
||||
)
|
||||
set("edit_send_id", (addSendType as? AddSendType.EditItem)?.sendItemId)
|
||||
set("activityToken", activityToken)
|
||||
},
|
||||
authRepo = authRepository,
|
||||
environmentRepo = environmentRepository,
|
||||
specialCircumstanceManager = specialCircumstanceManager,
|
||||
clock = clock,
|
||||
clipboardManager = clipboardManager,
|
||||
vaultRepo = vaultRepository,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
package com.x8bit.bitwarden.ui.tools.feature.send.addsend.util
|
||||
|
||||
import android.net.Uri
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance
|
||||
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
|
||||
import com.x8bit.bitwarden.ui.tools.feature.send.addsend.AddSendState
|
||||
import io.mockk.mockk
|
||||
|
@ -20,7 +20,7 @@ class SpecialCircumstanceExtensionsTest {
|
|||
input = text,
|
||||
isHideByDefaultChecked = false,
|
||||
)
|
||||
val specialCircumstance = UserState.SpecialCircumstance.ShareNewSend(
|
||||
val specialCircumstance = SpecialCircumstance.ShareNewSend(
|
||||
data = IntentManager.ShareData.TextSend(
|
||||
subject = "",
|
||||
text = text,
|
||||
|
@ -44,7 +44,7 @@ class SpecialCircumstanceExtensionsTest {
|
|||
sizeBytes = sizeBytes,
|
||||
displaySize = null,
|
||||
)
|
||||
val specialCircumstance = UserState.SpecialCircumstance.ShareNewSend(
|
||||
val specialCircumstance = SpecialCircumstance.ShareNewSend(
|
||||
data = IntentManager.ShareData.FileSend(
|
||||
fileData = IntentManager.FileData(
|
||||
fileName = fileName,
|
||||
|
@ -62,14 +62,14 @@ class SpecialCircumstanceExtensionsTest {
|
|||
|
||||
@Test
|
||||
fun `toSendType with null SpecialCircumstance should return null`() {
|
||||
val specialCircumstance: UserState.SpecialCircumstance? = null
|
||||
val specialCircumstance: SpecialCircumstance? = null
|
||||
assertNull(specialCircumstance.toSendType())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toSendName with TextSend should return subject`() {
|
||||
val subject = "Subject"
|
||||
val specialCircumstance = UserState.SpecialCircumstance.ShareNewSend(
|
||||
val specialCircumstance = SpecialCircumstance.ShareNewSend(
|
||||
data = IntentManager.ShareData.TextSend(
|
||||
subject = subject,
|
||||
text = "",
|
||||
|
@ -85,7 +85,7 @@ class SpecialCircumstanceExtensionsTest {
|
|||
@Test
|
||||
fun `toSendName with FileSend should return file name`() {
|
||||
val fileName = "File Name"
|
||||
val specialCircumstance = UserState.SpecialCircumstance.ShareNewSend(
|
||||
val specialCircumstance = SpecialCircumstance.ShareNewSend(
|
||||
data = IntentManager.ShareData.FileSend(
|
||||
fileData = IntentManager.FileData(
|
||||
fileName = fileName,
|
||||
|
@ -103,14 +103,14 @@ class SpecialCircumstanceExtensionsTest {
|
|||
|
||||
@Test
|
||||
fun `toSendName with null SpecialCircumstance should return null`() {
|
||||
val specialCircumstance: UserState.SpecialCircumstance? = null
|
||||
val specialCircumstance: SpecialCircumstance? = null
|
||||
assertNull(specialCircumstance.toSendName())
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `shouldFinishOnComplete with ShareNewSend shouldFinishWhenComplete true should return true`() {
|
||||
val specialCircumstance = UserState.SpecialCircumstance.ShareNewSend(
|
||||
val specialCircumstance = SpecialCircumstance.ShareNewSend(
|
||||
data = mockk(),
|
||||
shouldFinishWhenComplete = true,
|
||||
)
|
||||
|
@ -120,7 +120,7 @@ class SpecialCircumstanceExtensionsTest {
|
|||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `shouldFinishOnComplete with ShareNewSend shouldFinishWhenComplete false should return false`() {
|
||||
val specialCircumstance = UserState.SpecialCircumstance.ShareNewSend(
|
||||
val specialCircumstance = SpecialCircumstance.ShareNewSend(
|
||||
data = mockk(),
|
||||
shouldFinishWhenComplete = false,
|
||||
)
|
||||
|
@ -129,7 +129,7 @@ class SpecialCircumstanceExtensionsTest {
|
|||
|
||||
@Test
|
||||
fun `shouldFinishOnComplete with null SpecialCircumstance should return false`() {
|
||||
val specialCircumstance: UserState.SpecialCircumstance? = null
|
||||
val specialCircumstance: SpecialCircumstance? = null
|
||||
assertFalse(specialCircumstance.shouldFinishOnComplete())
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue