mirror of
https://github.com/bitwarden/android.git
synced 2026-06-13 16:14:02 -05:00
Compare commits
9 Commits
PM-39006-s
...
sdlc/sdk-u
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c370f5cc88 | ||
|
|
bd69b2acdb | ||
|
|
de80c09240 | ||
|
|
3a125605a4 | ||
|
|
cac951d6c2 | ||
|
|
4516ae9eac | ||
|
|
cada80b4b8 | ||
|
|
b7a2691804 | ||
|
|
2c2c534bcd |
@@ -15,13 +15,13 @@ import androidx.browser.auth.AuthTabIntent
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.os.LocaleListCompat
|
||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.compose.NavHost
|
||||
import com.bitwarden.annotation.OmitFromCoverage
|
||||
import com.bitwarden.ui.platform.base.util.EventsEffect
|
||||
@@ -36,11 +36,17 @@ import com.x8bit.bitwarden.data.platform.manager.util.ObserveScreenDataEffect
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
import com.x8bit.bitwarden.ui.platform.components.util.rememberBitwardenNavController
|
||||
import com.x8bit.bitwarden.ui.platform.composition.LocalManagerProvider
|
||||
import com.x8bit.bitwarden.ui.platform.feature.accessibilitydisclosure.accessibilityDisclosureDestination
|
||||
import com.x8bit.bitwarden.ui.platform.feature.accessibilitydisclosure.navigateToAccessibilityDisclosure
|
||||
import com.x8bit.bitwarden.ui.platform.feature.cookieacquisition.cookieAcquisitionDestination
|
||||
import com.x8bit.bitwarden.ui.platform.feature.cookieacquisition.navigateToCookieAcquisition
|
||||
import com.x8bit.bitwarden.ui.platform.feature.debugmenu.debugMenuDestination
|
||||
import com.x8bit.bitwarden.ui.platform.feature.debugmenu.manager.DebugMenuLaunchManager
|
||||
import com.x8bit.bitwarden.ui.platform.feature.debugmenu.navigateToDebugMenuScreen
|
||||
import com.x8bit.bitwarden.ui.platform.feature.overlaynav.OverlayNavRoute
|
||||
import com.x8bit.bitwarden.ui.platform.feature.overlaynav.overlayNavDestination
|
||||
import com.x8bit.bitwarden.ui.platform.feature.localnetworkaccess.localNetworkAccessDestination
|
||||
import com.x8bit.bitwarden.ui.platform.feature.localnetworkaccess.navigateToLocalNetworkAccess
|
||||
import com.x8bit.bitwarden.ui.platform.feature.rootnav.RootNavigationRoute
|
||||
import com.x8bit.bitwarden.ui.platform.feature.rootnav.rootNavDestination
|
||||
import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.model.AppLanguage
|
||||
import com.x8bit.bitwarden.ui.platform.model.AuthTabLaunchers
|
||||
import com.x8bit.bitwarden.ui.platform.util.appLanguage
|
||||
@@ -106,6 +112,7 @@ class MainActivity : AppCompatActivity() {
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
intent = intent.validate()
|
||||
var shouldShowSplashScreen = true
|
||||
@@ -126,13 +133,49 @@ class MainActivity : AppCompatActivity() {
|
||||
SetupEventsEffect(navController = navController)
|
||||
val state by mainViewModel.stateFlow.collectAsStateWithLifecycle()
|
||||
updateScreenCapture(isScreenCaptureAllowed = state.isScreenCaptureAllowed)
|
||||
MainActivityContent(
|
||||
state = state,
|
||||
LocalManagerProvider(
|
||||
featureFlagsState = state.featureFlagsState,
|
||||
authTabLaunchers = authTabLaunchers,
|
||||
navController = navController,
|
||||
sendAction = mainViewModel::trySendAction,
|
||||
onSplashScreenRemoved = { shouldShowSplashScreen = false },
|
||||
)
|
||||
) {
|
||||
ObserveScreenDataEffect(
|
||||
onDataUpdate = remember(mainViewModel) {
|
||||
{ mainViewModel.trySendAction(MainAction.ResumeScreenDataReceived(it)) }
|
||||
},
|
||||
)
|
||||
BitwardenTheme(
|
||||
theme = state.theme,
|
||||
dynamicColor = state.isDynamicColorsEnabled,
|
||||
) {
|
||||
NavHost(
|
||||
navController = navController,
|
||||
startDestination = RootNavigationRoute,
|
||||
modifier = Modifier
|
||||
.background(color = BitwardenTheme.colorScheme.background.primary),
|
||||
) {
|
||||
// Root navigation, debug menu, and cookie acquisition exist at
|
||||
// this top level. They can appear on top of the rest of the app
|
||||
// without interacting with the state-based navigation used by
|
||||
// RootNavScreen.
|
||||
rootNavDestination { shouldShowSplashScreen = false }
|
||||
debugMenuDestination(
|
||||
onNavigateBack = { navController.popBackStack() },
|
||||
onSplashScreenRemoved = { shouldShowSplashScreen = false },
|
||||
)
|
||||
cookieAcquisitionDestination(
|
||||
onDismiss = { navController.popBackStack() },
|
||||
onSplashScreenRemoved = { shouldShowSplashScreen = false },
|
||||
)
|
||||
localNetworkAccessDestination(
|
||||
onDismiss = { navController.popBackStack() },
|
||||
onSplashScreenRemoved = { shouldShowSplashScreen = false },
|
||||
)
|
||||
accessibilityDisclosureDestination(
|
||||
onDismiss = { navController.popBackStack() },
|
||||
onSplashScreenRemoved = { shouldShowSplashScreen = false },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,7 +207,7 @@ class MainActivity : AppCompatActivity() {
|
||||
locales.get(0)?.appLanguage
|
||||
}
|
||||
} else {
|
||||
// For older versions, use whatever language is available from the repository.
|
||||
// For older versions, use what ever language is available from the repository.
|
||||
settingsRepository.appLanguage
|
||||
}
|
||||
|
||||
@@ -213,6 +256,15 @@ class MainActivity : AppCompatActivity() {
|
||||
is MainEvent.CompleteAutofill -> handleCompleteAutofill(event)
|
||||
MainEvent.Recreate -> handleRecreate()
|
||||
MainEvent.NavigateToDebugMenu -> navController.navigateToDebugMenuScreen()
|
||||
MainEvent.NavigateToCookieAcquisition -> navController.navigateToCookieAcquisition()
|
||||
MainEvent.NavigateToLocalNetworkAccess -> {
|
||||
navController.navigateToLocalNetworkAccess()
|
||||
}
|
||||
|
||||
MainEvent.NavigateToAccessibilityDisclosure -> {
|
||||
navController.navigateToAccessibilityDisclosure()
|
||||
}
|
||||
|
||||
is MainEvent.UpdateAppLocale -> {
|
||||
AppCompatDelegate.setApplicationLocales(
|
||||
LocaleListCompat.forLanguageTags(event.localeName),
|
||||
@@ -256,38 +308,3 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OmitFromCoverage
|
||||
@Composable
|
||||
private fun MainActivityContent(
|
||||
state: MainState,
|
||||
authTabLaunchers: AuthTabLaunchers,
|
||||
navController: NavHostController,
|
||||
sendAction: (MainAction) -> Unit,
|
||||
onSplashScreenRemoved: () -> Unit,
|
||||
) {
|
||||
LocalManagerProvider(
|
||||
featureFlagsState = state.featureFlagsState,
|
||||
authTabLaunchers = authTabLaunchers,
|
||||
) {
|
||||
ObserveScreenDataEffect { sendAction(MainAction.ResumeScreenDataReceived(it)) }
|
||||
BitwardenTheme(
|
||||
theme = state.theme,
|
||||
dynamicColor = state.isDynamicColorsEnabled,
|
||||
) {
|
||||
NavHost(
|
||||
navController = navController,
|
||||
startDestination = OverlayNavRoute,
|
||||
modifier = Modifier.background(BitwardenTheme.colorScheme.background.primary),
|
||||
) {
|
||||
// The OverlayNav and Debug destinations are the only UIs that can be
|
||||
// displayed here, everything else should be inside the OverlayNav.
|
||||
overlayNavDestination(onSplashScreenRemoved = onSplashScreenRemoved)
|
||||
debugMenuDestination(
|
||||
onNavigateBack = { navController.popBackStack() },
|
||||
onSplashScreenRemoved = onSplashScreenRemoved,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,11 +31,13 @@ import com.x8bit.bitwarden.data.billing.util.getPremiumCheckoutCallbackResult
|
||||
import com.x8bit.bitwarden.data.credentials.manager.CredentialProviderRequestManager
|
||||
import com.x8bit.bitwarden.data.credentials.manager.model.CredentialProviderRequest
|
||||
import com.x8bit.bitwarden.data.platform.manager.AppResumeManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.CookieAcquisitionRequestManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.garbage.GarbageCollectionManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.AppResumeScreenData
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.CompleteRegistrationData
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance
|
||||
import com.x8bit.bitwarden.data.platform.manager.network.NetworkPermissionManager
|
||||
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
import com.x8bit.bitwarden.data.platform.util.isAddTotpLoginItemFromAuthenticator
|
||||
@@ -55,6 +57,7 @@ import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.drop
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
@@ -78,6 +81,8 @@ private const val ANIMATION_DEBOUNCE_DELAY_MS = 500L
|
||||
class MainViewModel @Inject constructor(
|
||||
accessibilitySelectionManager: AccessibilitySelectionManager,
|
||||
autofillSelectionManager: AutofillSelectionManager,
|
||||
cookieAcquisitionRequestManager: CookieAcquisitionRequestManager,
|
||||
networkPermissionManager: NetworkPermissionManager,
|
||||
private val addTotpItemFromAuthenticatorManager: AddTotpItemFromAuthenticatorManager,
|
||||
private val specialCircumstanceManager: SpecialCircumstanceManager,
|
||||
private val garbageCollectionManager: GarbageCollectionManager,
|
||||
@@ -147,6 +152,12 @@ class MainViewModel @Inject constructor(
|
||||
.onEach(::trySendAction)
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
settingsRepository
|
||||
.hasShownAccessibilityDisclaimerFlow
|
||||
.map { MainAction.Internal.HasShownAccessibilityDisclaimerUpdate(it) }
|
||||
.onEach(::trySendAction)
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
merge(
|
||||
authRepository
|
||||
.userStateFlow
|
||||
@@ -166,6 +177,20 @@ class MainViewModel @Inject constructor(
|
||||
.onEach(::sendAction)
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
networkPermissionManager
|
||||
.isLocalNetworkAccessRequiredStateFlow
|
||||
.filter { it }
|
||||
.map { MainAction.Internal.LocalNetworkAccessRequired }
|
||||
.onEach(::sendAction)
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
cookieAcquisitionRequestManager
|
||||
.cookieAcquisitionRequestFlow
|
||||
.filterNotNull()
|
||||
.map { MainAction.Internal.CookieAcquisitionReady }
|
||||
.onEach(::sendAction)
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
// On app launch, mark all active users as having previously logged in.
|
||||
// This covers any users who are active prior to this value being recorded.
|
||||
viewModelScope.launch {
|
||||
@@ -214,7 +239,20 @@ class MainViewModel @Inject constructor(
|
||||
is MainAction.Internal.ScreenCaptureUpdate -> handleScreenCaptureUpdate(action)
|
||||
is MainAction.Internal.ThemeUpdate -> handleAppThemeUpdated(action)
|
||||
is MainAction.Internal.DynamicColorsUpdate -> handleDynamicColorsUpdate(action)
|
||||
is MainAction.Internal.CookieAcquisitionReady -> handleCookieAcquisitionReady()
|
||||
is MainAction.Internal.LocalNetworkAccessRequired -> handleLocalNetworkAccessRequired()
|
||||
is MainAction.Internal.ResizeHasBeenRequested -> handleResizeHasBeenRequested()
|
||||
is MainAction.Internal.HasShownAccessibilityDisclaimerUpdate -> {
|
||||
handleHasShownAccessibilityDisclaimerUpdate(action)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleHasShownAccessibilityDisclaimerUpdate(
|
||||
action: MainAction.Internal.HasShownAccessibilityDisclaimerUpdate,
|
||||
) {
|
||||
if (!action.hasBeenShown) {
|
||||
sendEvent(MainEvent.NavigateToAccessibilityDisclosure)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -295,6 +333,14 @@ class MainViewModel @Inject constructor(
|
||||
mutableStateFlow.update { it.copy(isDynamicColorsEnabled = action.isDynamicColorsEnabled) }
|
||||
}
|
||||
|
||||
private fun handleCookieAcquisitionReady() {
|
||||
sendEvent(MainEvent.NavigateToCookieAcquisition)
|
||||
}
|
||||
|
||||
private fun handleLocalNetworkAccessRequired() {
|
||||
sendEvent(MainEvent.NavigateToLocalNetworkAccess)
|
||||
}
|
||||
|
||||
private fun handleResizeHasBeenRequested() {
|
||||
mutableStateFlow.update { it.copy(hasResizeBeenRequested = true) }
|
||||
}
|
||||
@@ -649,10 +695,26 @@ sealed class MainAction {
|
||||
val isDynamicColorsEnabled: Boolean,
|
||||
) : Internal()
|
||||
|
||||
/**
|
||||
* Indicates that the cookie acquisition conditions are met and navigation
|
||||
* should proceed.
|
||||
*/
|
||||
data object CookieAcquisitionReady : Internal()
|
||||
|
||||
/**
|
||||
* Indicates that the local network access is required.
|
||||
*/
|
||||
data object LocalNetworkAccessRequired : Internal()
|
||||
|
||||
/**
|
||||
* Indicates that resize has been requested on the Activity
|
||||
*/
|
||||
data object ResizeHasBeenRequested : Internal()
|
||||
|
||||
/**
|
||||
* Indicates that the accessibility disclaimer has been displayed.
|
||||
*/
|
||||
data class HasShownAccessibilityDisclaimerUpdate(val hasBeenShown: Boolean) : Internal()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -682,6 +744,21 @@ sealed class MainEvent {
|
||||
*/
|
||||
data object NavigateToDebugMenu : MainEvent()
|
||||
|
||||
/**
|
||||
* Navigate to the cookie acquisition screen.
|
||||
*/
|
||||
data object NavigateToCookieAcquisition : MainEvent()
|
||||
|
||||
/**
|
||||
* Navigate to the local network access screen.
|
||||
*/
|
||||
data object NavigateToLocalNetworkAccess : MainEvent()
|
||||
|
||||
/**
|
||||
* Navigate to the accessibility disclosure screen.
|
||||
*/
|
||||
data object NavigateToAccessibilityDisclosure : MainEvent()
|
||||
|
||||
/**
|
||||
* Indicates that the app language has been updated.
|
||||
*/
|
||||
|
||||
@@ -2,7 +2,6 @@ package com.x8bit.bitwarden.data.auth.repository
|
||||
|
||||
import com.bitwarden.core.AuthRequestMethod
|
||||
import com.bitwarden.core.InitUserCryptoMethod
|
||||
import com.bitwarden.core.MasterPasswordUnlockData
|
||||
import com.bitwarden.core.RegisterTdeKeyResponse
|
||||
import com.bitwarden.core.WrappedAccountCryptographicState
|
||||
import com.bitwarden.core.data.manager.dispatcher.DispatcherManager
|
||||
@@ -1146,10 +1145,10 @@ class AuthRepositoryImpl(
|
||||
organizationIdentifier: String,
|
||||
password: String,
|
||||
passwordHint: String?,
|
||||
): SetPasswordResult = userStateManager.userStateTransaction {
|
||||
): SetPasswordResult {
|
||||
val profile = authDiskSource.userState?.activeAccount?.profile
|
||||
?: return@userStateTransaction SetPasswordResult.Error(error = NoActiveUserException())
|
||||
return@userStateTransaction when (profile.forcePasswordResetReason) {
|
||||
?: return SetPasswordResult.Error(error = NoActiveUserException())
|
||||
return when (profile.forcePasswordResetReason) {
|
||||
ForcePasswordResetReason.TDE_USER_WITHOUT_PASSWORD_HAS_PASSWORD_RESET_PERMISSION -> {
|
||||
setUpdatedPassword(
|
||||
profile = profile,
|
||||
@@ -1197,25 +1196,30 @@ class AuthRepositoryImpl(
|
||||
keys = null,
|
||||
),
|
||||
)
|
||||
.map { response }
|
||||
.map { response.passwordHash }
|
||||
}
|
||||
.onSuccess { response ->
|
||||
.flatMap { masterPasswordHash ->
|
||||
when (val result = vaultRepository.unlockVaultWithMasterPassword(password)) {
|
||||
is VaultUnlockResult.Success -> {
|
||||
enrollUserInPasswordReset(
|
||||
userId = userId,
|
||||
organizationIdentifier = organizationIdentifier,
|
||||
passwordHash = masterPasswordHash,
|
||||
)
|
||||
}
|
||||
|
||||
is VaultUnlockError -> {
|
||||
(result.error ?: IllegalStateException("Failed to unlock vault"))
|
||||
.asFailure()
|
||||
}
|
||||
}
|
||||
}
|
||||
.onSuccess {
|
||||
authDiskSource.userState = authDiskSource.userState?.toUserStateJsonWithPassword(
|
||||
masterPasswordUnlock = MasterPasswordUnlockData(
|
||||
kdf = profile.toSdkParams(),
|
||||
masterKeyWrappedUserKey = response.newKey,
|
||||
salt = profile.email,
|
||||
),
|
||||
masterPasswordUnlock = null,
|
||||
)
|
||||
this.organizationIdentifier = null
|
||||
}
|
||||
.flatMap { response ->
|
||||
enrollUserInPasswordReset(
|
||||
userId = userId,
|
||||
organizationIdentifier = organizationIdentifier,
|
||||
passwordHash = response.passwordHash,
|
||||
)
|
||||
}
|
||||
.fold(
|
||||
onFailure = { SetPasswordResult.Error(error = it) },
|
||||
onSuccess = { SetPasswordResult.Success },
|
||||
@@ -1322,26 +1326,16 @@ class AuthRepositoryImpl(
|
||||
privateKey = response.keys.private,
|
||||
),
|
||||
)
|
||||
authDiskSource.userState = authDiskSource
|
||||
.userState
|
||||
?.toUserStateJsonWithPassword(
|
||||
masterPasswordUnlock = MasterPasswordUnlockData(
|
||||
kdf = profile.toSdkParams(),
|
||||
masterKeyWrappedUserKey = response.encryptedUserKey,
|
||||
salt = profile.email,
|
||||
),
|
||||
)
|
||||
this.organizationIdentifier = null
|
||||
}
|
||||
.map { response }
|
||||
.map { response.masterPasswordHash }
|
||||
}
|
||||
.flatMap { response ->
|
||||
.flatMap { masterPasswordHash ->
|
||||
when (val result = vaultRepository.unlockVaultWithMasterPassword(password)) {
|
||||
is VaultUnlockResult.Success -> {
|
||||
enrollUserInPasswordReset(
|
||||
userId = userId,
|
||||
organizationIdentifier = organizationIdentifier,
|
||||
passwordHash = response.masterPasswordHash,
|
||||
passwordHash = masterPasswordHash,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1351,6 +1345,12 @@ class AuthRepositoryImpl(
|
||||
}
|
||||
}
|
||||
}
|
||||
.onSuccess {
|
||||
authDiskSource.userState = authDiskSource.userState?.toUserStateJsonWithPassword(
|
||||
masterPasswordUnlock = null,
|
||||
)
|
||||
this.organizationIdentifier = null
|
||||
}
|
||||
.fold(
|
||||
onFailure = { SetPasswordResult.Error(error = it) },
|
||||
onSuccess = { SetPasswordResult.Success },
|
||||
|
||||
@@ -10,7 +10,6 @@ import com.bitwarden.network.model.UserDecryptionOptionsJson
|
||||
import com.bitwarden.policies.PolicyType
|
||||
import com.bitwarden.policies.PolicyView
|
||||
import com.bitwarden.ui.platform.base.util.toHexColorRepresentation
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.ForcePasswordResetReason
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.sdk.util.toKdfRequestModel
|
||||
@@ -85,17 +84,10 @@ fun UserStateJson.toUpdatedUserStateJson(
|
||||
}
|
||||
?: profile
|
||||
.userDecryptionOptions
|
||||
?.copy(
|
||||
hasMasterPassword = false,
|
||||
masterPasswordUnlock = null,
|
||||
)
|
||||
val forcePasswordResetReason = syncProfile.getForcePasswordResetReason(
|
||||
userDecryptionOptions = userDecryptionOptions,
|
||||
previousForcePasswordResetReason = profile.forcePasswordResetReason,
|
||||
)
|
||||
?.copy(masterPasswordUnlock = null)
|
||||
|
||||
val updatedProfile = profile
|
||||
.copy(
|
||||
forcePasswordResetReason = forcePasswordResetReason,
|
||||
avatarColorHex = syncProfile.avatarColor,
|
||||
stamp = syncProfile.securityStamp,
|
||||
hasPremiumPersonally = syncProfile.isPremium,
|
||||
@@ -103,31 +95,24 @@ fun UserStateJson.toUpdatedUserStateJson(
|
||||
isTwoFactorEnabled = syncProfile.isTwoFactorEnabled,
|
||||
creationDate = syncProfile.creationDate,
|
||||
userDecryptionOptions = userDecryptionOptions,
|
||||
kdfType = masterPasswordUnlockKdf?.kdfType ?: profile.kdfType,
|
||||
kdfIterations = masterPasswordUnlockKdf?.iterations ?: profile.kdfIterations,
|
||||
kdfMemory = masterPasswordUnlockKdf?.memory ?: profile.kdfMemory,
|
||||
kdfParallelism = masterPasswordUnlockKdf?.parallelism ?: profile.kdfParallelism,
|
||||
kdfType = masterPasswordUnlockKdf?.kdfType
|
||||
?: profile.kdfType,
|
||||
kdfIterations = masterPasswordUnlockKdf?.iterations
|
||||
?: profile.kdfIterations,
|
||||
kdfMemory = masterPasswordUnlockKdf?.memory
|
||||
?: profile.kdfMemory,
|
||||
kdfParallelism = masterPasswordUnlockKdf?.parallelism
|
||||
?: profile.kdfParallelism,
|
||||
)
|
||||
val updatedAccount = account.copy(profile = updatedProfile)
|
||||
return this.copy(accounts = accounts.toMutableMap().apply { replace(userId, updatedAccount) })
|
||||
}
|
||||
|
||||
private fun SyncResponseJson.Profile.getForcePasswordResetReason(
|
||||
userDecryptionOptions: UserDecryptionOptionsJson?,
|
||||
previousForcePasswordResetReason: ForcePasswordResetReason?,
|
||||
): ForcePasswordResetReason? {
|
||||
val hasManageResetPasswordPermission = this.organizations.orEmpty().any {
|
||||
it.type == OrganizationType.OWNER ||
|
||||
it.type == OrganizationType.ADMIN ||
|
||||
it.permissions.shouldManageResetPassword
|
||||
}
|
||||
return ForcePasswordResetReason
|
||||
.TDE_USER_WITHOUT_PASSWORD_HAS_PASSWORD_RESET_PERMISSION
|
||||
.takeIf {
|
||||
userDecryptionOptions?.hasMasterPassword == false &&
|
||||
hasManageResetPasswordPermission
|
||||
}
|
||||
?: previousForcePasswordResetReason
|
||||
return this
|
||||
.copy(
|
||||
accounts = accounts
|
||||
.toMutableMap()
|
||||
.apply {
|
||||
replace(userId, updatedAccount)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -135,16 +120,20 @@ private fun SyncResponseJson.Profile.getForcePasswordResetReason(
|
||||
* their password.
|
||||
*/
|
||||
fun UserStateJson.toUserStateJsonWithPassword(
|
||||
masterPasswordUnlock: MasterPasswordUnlockData,
|
||||
masterPasswordUnlock: MasterPasswordUnlockData?,
|
||||
): UserStateJson {
|
||||
val account = this.activeAccount
|
||||
val profile = account.profile
|
||||
val userDecryptionOptions = profile.userDecryptionOptions
|
||||
val masterPasswordUnlockJson = MasterPasswordUnlockDataJson(
|
||||
salt = masterPasswordUnlock.salt,
|
||||
kdf = masterPasswordUnlock.kdf.toKdfRequestModel(),
|
||||
masterKeyWrappedUserKey = masterPasswordUnlock.masterKeyWrappedUserKey,
|
||||
)
|
||||
val masterPasswordUnlockJson = masterPasswordUnlock
|
||||
?.let {
|
||||
MasterPasswordUnlockDataJson(
|
||||
salt = it.salt,
|
||||
kdf = it.kdf.toKdfRequestModel(),
|
||||
masterKeyWrappedUserKey = it.masterKeyWrappedUserKey,
|
||||
)
|
||||
}
|
||||
?: userDecryptionOptions?.masterPasswordUnlock
|
||||
val updatedProfile = profile
|
||||
.copy(
|
||||
forcePasswordResetReason = null,
|
||||
|
||||
@@ -6,12 +6,8 @@ import com.bitwarden.network.BitwardenServiceClient
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
|
||||
private const val ENVIRONMENT_DEBOUNCE_TIMEOUT_MS: Long = 500L
|
||||
@@ -30,11 +26,9 @@ class NetworkConfigManagerImpl(
|
||||
private val collectionScope = CoroutineScope(dispatcherManager.unconfined)
|
||||
|
||||
init {
|
||||
@OptIn(FlowPreview::class)
|
||||
combine(
|
||||
environmentRepository.environmentStateFlow,
|
||||
authRepository.userStateFlow.map { it?.activeUserId }.distinctUntilChanged(),
|
||||
) { _, _ -> }
|
||||
@Suppress("OPT_IN_USAGE")
|
||||
environmentRepository
|
||||
.environmentStateFlow
|
||||
.debounce(timeoutMillis = ENVIRONMENT_DEBOUNCE_TIMEOUT_MS)
|
||||
.onEach { _ ->
|
||||
// This updates the stored service configuration by performing a network request.
|
||||
|
||||
@@ -2,7 +2,7 @@ package com.x8bit.bitwarden.ui.platform.feature.debugmenu
|
||||
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import com.bitwarden.ui.platform.base.util.composableWithSlideTransitions
|
||||
import com.bitwarden.ui.platform.base.util.composableWithPushTransitions
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
@@ -27,7 +27,7 @@ fun NavGraphBuilder.debugMenuDestination(
|
||||
onNavigateBack: () -> Unit,
|
||||
onSplashScreenRemoved: () -> Unit,
|
||||
) {
|
||||
composableWithSlideTransitions<DebugRoute> {
|
||||
composableWithPushTransitions<DebugRoute> {
|
||||
DebugMenuScreen(onNavigateBack = onNavigateBack)
|
||||
// If we are displaying the debug screen, then we can just hide the splash screen.
|
||||
onSplashScreenRemoved()
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
package com.x8bit.bitwarden.ui.platform.feature.overlaynav
|
||||
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.compose.composable
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* The type-safe route for the overlay navigation screen.
|
||||
*/
|
||||
@Serializable
|
||||
data object OverlayNavRoute
|
||||
|
||||
/**
|
||||
* Add the overlay navigation screen to the nav graph.
|
||||
*/
|
||||
fun NavGraphBuilder.overlayNavDestination(
|
||||
onSplashScreenRemoved: () -> Unit,
|
||||
) {
|
||||
composable<OverlayNavRoute> {
|
||||
OverlayNavScreen(onSplashScreenRemoved = onSplashScreenRemoved)
|
||||
}
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
package com.x8bit.bitwarden.ui.platform.feature.overlaynav
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.compose.NavHost
|
||||
import com.bitwarden.ui.platform.base.util.EventsEffect
|
||||
import com.x8bit.bitwarden.ui.platform.components.util.rememberBitwardenNavController
|
||||
import com.x8bit.bitwarden.ui.platform.feature.accessibilitydisclosure.accessibilityDisclosureDestination
|
||||
import com.x8bit.bitwarden.ui.platform.feature.accessibilitydisclosure.navigateToAccessibilityDisclosure
|
||||
import com.x8bit.bitwarden.ui.platform.feature.cookieacquisition.cookieAcquisitionDestination
|
||||
import com.x8bit.bitwarden.ui.platform.feature.cookieacquisition.navigateToCookieAcquisition
|
||||
import com.x8bit.bitwarden.ui.platform.feature.localnetworkaccess.localNetworkAccessDestination
|
||||
import com.x8bit.bitwarden.ui.platform.feature.localnetworkaccess.navigateToLocalNetworkAccess
|
||||
import com.x8bit.bitwarden.ui.platform.feature.rootnav.RootNavigationRoute
|
||||
import com.x8bit.bitwarden.ui.platform.feature.rootnav.rootNavDestination
|
||||
|
||||
/**
|
||||
* Controls the overlay [NavHost] for the app including the [rootNavDestination] and any screen
|
||||
* that can appear on top of it without affecting its state.
|
||||
*/
|
||||
@Composable
|
||||
fun OverlayNavScreen(
|
||||
viewModel: OverlayNavViewModel = hiltViewModel(),
|
||||
navController: NavHostController = rememberBitwardenNavController(name = "OverlayNavScreen"),
|
||||
onSplashScreenRemoved: () -> Unit,
|
||||
) {
|
||||
OverlayNavEventsEffect(
|
||||
viewModel = viewModel,
|
||||
navController = navController,
|
||||
)
|
||||
NavHost(
|
||||
navController = navController,
|
||||
startDestination = RootNavigationRoute,
|
||||
) {
|
||||
// This is the overlay level of navigation that sits above the root nav. These screens
|
||||
// can appear on top of the rest of the app without interacting with the state-based
|
||||
// navigation used by RootNavScreen (which also exists here).
|
||||
rootNavDestination(onSplashScreenRemoved = onSplashScreenRemoved)
|
||||
cookieAcquisitionDestination(
|
||||
onDismiss = { navController.popBackStack() },
|
||||
onSplashScreenRemoved = onSplashScreenRemoved,
|
||||
)
|
||||
localNetworkAccessDestination(
|
||||
onDismiss = { navController.popBackStack() },
|
||||
onSplashScreenRemoved = onSplashScreenRemoved,
|
||||
)
|
||||
accessibilityDisclosureDestination(
|
||||
onDismiss = { navController.popBackStack() },
|
||||
onSplashScreenRemoved = onSplashScreenRemoved,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun OverlayNavEventsEffect(
|
||||
viewModel: OverlayNavViewModel,
|
||||
navController: NavController,
|
||||
) {
|
||||
EventsEffect(viewModel = viewModel) { event ->
|
||||
when (event) {
|
||||
OverlayNavEvent.NavigateToCookieAcquisition -> {
|
||||
navController.navigateToCookieAcquisition()
|
||||
}
|
||||
|
||||
OverlayNavEvent.NavigateToLocalNetworkAccess -> {
|
||||
navController.navigateToLocalNetworkAccess()
|
||||
}
|
||||
|
||||
OverlayNavEvent.NavigateToAccessibilityDisclosure -> {
|
||||
navController.navigateToAccessibilityDisclosure()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,127 +0,0 @@
|
||||
package com.x8bit.bitwarden.ui.platform.feature.overlaynav
|
||||
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.bitwarden.ui.platform.base.BaseViewModel
|
||||
import com.bitwarden.ui.platform.base.DeferredBackgroundEvent
|
||||
import com.x8bit.bitwarden.data.platform.manager.CookieAcquisitionRequestManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.network.NetworkPermissionManager
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.filterNot
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Manages the overlay navigation, hosting the root-navigation and any screen that can overlay it.
|
||||
*/
|
||||
@HiltViewModel
|
||||
class OverlayNavViewModel @Inject constructor(
|
||||
cookieAcquisitionRequestManager: CookieAcquisitionRequestManager,
|
||||
networkPermissionManager: NetworkPermissionManager,
|
||||
settingsRepository: SettingsRepository,
|
||||
) : BaseViewModel<Unit, OverlayNavEvent, OverlayNavAction>(initialState = Unit) {
|
||||
init {
|
||||
settingsRepository
|
||||
.hasShownAccessibilityDisclaimerFlow
|
||||
.filterNot { it }
|
||||
.map { OverlayNavAction.Internal.AccessibilityDisclosureRequired }
|
||||
.onEach(::trySendAction)
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
networkPermissionManager
|
||||
.isLocalNetworkAccessRequiredStateFlow
|
||||
.filter { it }
|
||||
.map { OverlayNavAction.Internal.LocalNetworkAccessRequired }
|
||||
.onEach(::sendAction)
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
cookieAcquisitionRequestManager
|
||||
.cookieAcquisitionRequestFlow
|
||||
.filterNotNull()
|
||||
.map { OverlayNavAction.Internal.CookieAcquisitionReady }
|
||||
.onEach(::sendAction)
|
||||
.launchIn(viewModelScope)
|
||||
}
|
||||
|
||||
override fun handleAction(action: OverlayNavAction) {
|
||||
when (action) {
|
||||
is OverlayNavAction.Internal -> handleInternal(action)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleInternal(action: OverlayNavAction.Internal) {
|
||||
when (action) {
|
||||
OverlayNavAction.Internal.AccessibilityDisclosureRequired -> {
|
||||
handleAccessibilityDisclosureRequired()
|
||||
}
|
||||
|
||||
OverlayNavAction.Internal.CookieAcquisitionReady -> handleCookieAcquisitionReady()
|
||||
OverlayNavAction.Internal.LocalNetworkAccessRequired -> {
|
||||
handleLocalNetworkAccessRequired()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleAccessibilityDisclosureRequired() {
|
||||
sendEvent(OverlayNavEvent.NavigateToAccessibilityDisclosure)
|
||||
}
|
||||
|
||||
private fun handleCookieAcquisitionReady() {
|
||||
sendEvent(OverlayNavEvent.NavigateToCookieAcquisition)
|
||||
}
|
||||
|
||||
private fun handleLocalNetworkAccessRequired() {
|
||||
sendEvent(OverlayNavEvent.NavigateToLocalNetworkAccess)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Models events for the overlay navigation screen.
|
||||
*/
|
||||
sealed class OverlayNavEvent {
|
||||
/**
|
||||
* Navigate to the cookie acquisition screen.
|
||||
*/
|
||||
data object NavigateToCookieAcquisition : OverlayNavEvent(), DeferredBackgroundEvent
|
||||
|
||||
/**
|
||||
* Navigate to the local network access screen.
|
||||
*/
|
||||
data object NavigateToLocalNetworkAccess : OverlayNavEvent(), DeferredBackgroundEvent
|
||||
|
||||
/**
|
||||
* Navigate to the accessibility disclosure screen.
|
||||
*/
|
||||
data object NavigateToAccessibilityDisclosure : OverlayNavEvent(), DeferredBackgroundEvent
|
||||
}
|
||||
|
||||
/**
|
||||
* Models actions for the overlay navigation screen.
|
||||
*/
|
||||
sealed class OverlayNavAction {
|
||||
/**
|
||||
* Internal ViewModel actions.
|
||||
*/
|
||||
sealed class Internal : OverlayNavAction() {
|
||||
|
||||
/**
|
||||
* Indicates that the cookie acquisition conditions are met and navigation
|
||||
* should proceed.
|
||||
*/
|
||||
data object CookieAcquisitionReady : Internal()
|
||||
|
||||
/**
|
||||
* Indicates that the local network access is required.
|
||||
*/
|
||||
data object LocalNetworkAccessRequired : Internal()
|
||||
|
||||
/**
|
||||
* Indicates that the accessibility disclosure needs to be displayed.
|
||||
*/
|
||||
data object AccessibilityDisclosureRequired : Internal()
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,9 @@ import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.rememberTopAppBarState
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -26,8 +28,6 @@ import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalResources
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.net.toUri
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
@@ -37,6 +37,7 @@ import com.bitwarden.ui.platform.base.util.standardHorizontalMargin
|
||||
import com.bitwarden.ui.platform.components.account.dialog.BitwardenLogoutConfirmationDialog
|
||||
import com.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
|
||||
import com.bitwarden.ui.platform.components.badge.NotificationBadge
|
||||
import com.bitwarden.ui.platform.components.button.BitwardenTextButton
|
||||
import com.bitwarden.ui.platform.components.card.BitwardenActionCard
|
||||
import com.bitwarden.ui.platform.components.card.actionCardExitAnimation
|
||||
import com.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog
|
||||
@@ -634,23 +635,50 @@ private fun FingerPrintPhraseDialog(
|
||||
onDismissRequest: () -> Unit,
|
||||
onLearnMore: () -> Unit,
|
||||
) {
|
||||
BitwardenTwoButtonDialog(
|
||||
title = stringResource(id = BitwardenString.fingerprint_phrase),
|
||||
message = buildAnnotatedString {
|
||||
append("${stringResource(id = BitwardenString.your_accounts_fingerprint)}:\n\n")
|
||||
withStyle(
|
||||
style = BitwardenTheme.typography.sensitiveInfoSmall
|
||||
.toSpanStyle()
|
||||
.copy(color = BitwardenTheme.colorScheme.text.codePink),
|
||||
) {
|
||||
append(fingerprintPhrase())
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
dismissButton = {
|
||||
BitwardenTextButton(
|
||||
label = stringResource(id = BitwardenString.close),
|
||||
onClick = onDismissRequest,
|
||||
)
|
||||
},
|
||||
confirmButton = {
|
||||
BitwardenTextButton(
|
||||
label = stringResource(id = BitwardenString.learn_more),
|
||||
isExternalLink = true,
|
||||
onClick = onLearnMore,
|
||||
)
|
||||
},
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(id = BitwardenString.fingerprint_phrase),
|
||||
color = BitwardenTheme.colorScheme.text.primary,
|
||||
style = BitwardenTheme.typography.headlineSmall,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
},
|
||||
text = {
|
||||
Column {
|
||||
Text(
|
||||
text = "${stringResource(id = BitwardenString.your_accounts_fingerprint)}:",
|
||||
color = BitwardenTheme.colorScheme.text.primary,
|
||||
style = BitwardenTheme.typography.bodyMedium,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
Text(
|
||||
text = fingerprintPhrase(),
|
||||
color = BitwardenTheme.colorScheme.text.codePink,
|
||||
style = BitwardenTheme.typography.sensitiveInfoSmall,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
},
|
||||
confirmButtonText = stringResource(id = BitwardenString.learn_more),
|
||||
dismissButtonText = stringResource(id = BitwardenString.close),
|
||||
onConfirmClick = onLearnMore,
|
||||
onDismissClick = onDismissRequest,
|
||||
onDismissRequest = onDismissRequest,
|
||||
containerColor = BitwardenTheme.colorScheme.background.primary,
|
||||
iconContentColor = BitwardenTheme.colorScheme.icon.secondary,
|
||||
titleContentColor = BitwardenTheme.colorScheme.text.primary,
|
||||
textContentColor = BitwardenTheme.colorScheme.text.primary,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.requiredHeightIn
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Text
|
||||
@@ -77,7 +78,7 @@ fun PinInputDialog(
|
||||
// This background is necessary for the dialog to not be transparent.
|
||||
.background(
|
||||
color = BitwardenTheme.colorScheme.background.primary,
|
||||
shape = BitwardenTheme.shapes.dialog,
|
||||
shape = RoundedCornerShape(28.dp),
|
||||
),
|
||||
horizontalAlignment = Alignment.End,
|
||||
) {
|
||||
|
||||
@@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.requiredHeightIn
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -56,7 +57,7 @@ fun AddEditBlockedUriDialog(
|
||||
// This background is necessary for the dialog to not be transparent.
|
||||
.background(
|
||||
color = BitwardenTheme.colorScheme.background.primary,
|
||||
shape = BitwardenTheme.shapes.dialog,
|
||||
shape = RoundedCornerShape(28.dp),
|
||||
),
|
||||
horizontalAlignment = Alignment.End,
|
||||
) {
|
||||
|
||||
@@ -255,7 +255,7 @@ private fun PrivilegedAppsListContent(
|
||||
) {
|
||||
BitwardenStandardIconButton(
|
||||
vectorIconRes = BitwardenDrawable.ic_delete,
|
||||
contentDescription = stringResource(id = BitwardenString.delete),
|
||||
contentDescription = "",
|
||||
onClick = remember(item) {
|
||||
{ onDeleteClick(item) }
|
||||
},
|
||||
|
||||
@@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
@@ -185,7 +186,7 @@ private fun ScanHintBanner(
|
||||
.semantics { liveRegion = LiveRegionMode.Polite }
|
||||
.background(
|
||||
color = BitwardenTheme.colorScheme.background.scrim,
|
||||
shape = BitwardenTheme.shapes.content,
|
||||
shape = RoundedCornerShape(size = 8.dp),
|
||||
)
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
)
|
||||
|
||||
@@ -163,21 +163,13 @@ fun VaultItemIdentityContent(
|
||||
}
|
||||
identityState.passportNumber?.let { passportNumber ->
|
||||
item(key = "passportNumber") {
|
||||
BitwardenPasswordField(
|
||||
IdentityCopyField(
|
||||
label = stringResource(id = BitwardenString.passport_number),
|
||||
value = passportNumber,
|
||||
onValueChange = {},
|
||||
readOnly = true,
|
||||
actions = {
|
||||
BitwardenStandardIconButton(
|
||||
vectorIconRes = BitwardenDrawable.ic_copy,
|
||||
contentDescription = stringResource(id = BitwardenString.copy_passport_number),
|
||||
onClick = vaultIdentityItemTypeHandlers.onCopyPassportNumberClick,
|
||||
modifier = Modifier.testTag(tag = "IdentityCopyPassportNumberButton"),
|
||||
)
|
||||
},
|
||||
passwordFieldTestTag = "IdentityPassportNumberEntry",
|
||||
showPasswordTestTag = "IdentityViewPassportNumberButton",
|
||||
copyContentDescription = stringResource(id = BitwardenString.copy_passport_number),
|
||||
textFieldTestTag = "IdentityPassportNumberEntry",
|
||||
copyActionTestTag = "IdentityCopyPassportNumberButton",
|
||||
onCopyClick = vaultIdentityItemTypeHandlers.onCopyPassportNumberClick,
|
||||
cardStyle = identityState
|
||||
.propertyList
|
||||
.toListItemCardStyle(
|
||||
|
||||
@@ -55,14 +55,17 @@ import com.x8bit.bitwarden.data.credentials.model.Fido2CredentialAssertionReques
|
||||
import com.x8bit.bitwarden.data.credentials.model.GetCredentialsRequest
|
||||
import com.x8bit.bitwarden.data.credentials.model.ProviderGetPasswordCredentialRequest
|
||||
import com.x8bit.bitwarden.data.platform.manager.AppResumeManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.CookieAcquisitionRequestManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManagerImpl
|
||||
import com.x8bit.bitwarden.data.platform.manager.garbage.GarbageCollectionManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.AppResumeScreenData
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.CompleteRegistrationData
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.CookieAcquisitionRequest
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.FirstTimeState
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.PasswordlessRequestData
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance
|
||||
import com.x8bit.bitwarden.data.platform.manager.network.NetworkPermissionManager
|
||||
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
import com.x8bit.bitwarden.data.platform.util.isAddTotpLoginItemFromAuthenticator
|
||||
@@ -111,6 +114,7 @@ class MainViewModelTest : BaseViewModelTest() {
|
||||
private val mutableAppLanguageFlow = MutableStateFlow(AppLanguage.DEFAULT)
|
||||
private val mutableScreenCaptureAllowedFlow = MutableStateFlow(true)
|
||||
private val mutableIsDynamicColorsEnabledFlow = MutableStateFlow(false)
|
||||
private val mutableHasShownAccessibilityDisclaimerFlow = MutableStateFlow(true)
|
||||
private val settingsRepository = mockk<SettingsRepository> {
|
||||
every { appTheme } returns AppTheme.DEFAULT
|
||||
every { appThemeStateFlow } returns mutableAppThemeFlow
|
||||
@@ -121,6 +125,10 @@ class MainViewModelTest : BaseViewModelTest() {
|
||||
every { appLanguage = any() } just runs
|
||||
every { isDynamicColorsEnabled } returns false
|
||||
every { isDynamicColorsEnabledFlow } returns mutableIsDynamicColorsEnabledFlow
|
||||
every {
|
||||
hasShownAccessibilityDisclaimerFlow
|
||||
} returns mutableHasShownAccessibilityDisclaimerFlow
|
||||
every { accessibilityDisclaimerHasBeenShown() } just runs
|
||||
}
|
||||
private val authRepository = mockk<AuthRepository> {
|
||||
every { activeUserId } returns DEFAULT_USER_STATE.activeUserId
|
||||
@@ -175,6 +183,17 @@ class MainViewModelTest : BaseViewModelTest() {
|
||||
every { show(message = any(), duration = any()) } just runs
|
||||
every { show(messageId = any(), duration = any()) } just runs
|
||||
}
|
||||
private val mutableCookieAcquisitionRequestFlow =
|
||||
MutableStateFlow<CookieAcquisitionRequest?>(null)
|
||||
private val cookieAcquisitionRequestManager: CookieAcquisitionRequestManager = mockk {
|
||||
every { cookieAcquisitionRequestFlow } returns mutableCookieAcquisitionRequestFlow
|
||||
}
|
||||
private val mutableIsLocalNetworkAccessRequiredStateFlow = MutableStateFlow(false)
|
||||
private val networkPermissionManager: NetworkPermissionManager = mockk {
|
||||
every {
|
||||
isLocalNetworkAccessRequiredStateFlow
|
||||
} returns mutableIsLocalNetworkAccessRequiredStateFlow
|
||||
}
|
||||
private val credentialProviderRequestManager: CredentialProviderRequestManager = mockk {
|
||||
every { getPendingCredentialRequest() } returns null
|
||||
}
|
||||
@@ -1300,6 +1319,53 @@ class MainViewModelTest : BaseViewModelTest() {
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `cookie acquisition should emit NavigateToCookieAcquisition when vault unlocked with matching hostname`() =
|
||||
runTest {
|
||||
mutableCookieAcquisitionRequestFlow.value = CookieAcquisitionRequest(
|
||||
hostname = DEFAULT_US_WEB_VAULT_URL,
|
||||
)
|
||||
val viewModel = createViewModel()
|
||||
|
||||
viewModel.eventFlow.test {
|
||||
// Skip init events (appLanguage + appTheme)
|
||||
skipItems(2)
|
||||
mutableUserStateFlow.value = DEFAULT_USER_STATE
|
||||
assertEquals(
|
||||
MainEvent.NavigateToCookieAcquisition,
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `local network access required should emit NavigateToLocalNetworkAccess when stateflow emits`() =
|
||||
runTest {
|
||||
mutableIsLocalNetworkAccessRequiredStateFlow.value = true
|
||||
val viewModel = createViewModel()
|
||||
|
||||
viewModel.eventFlow.test {
|
||||
// Skip init events (appLanguage + appTheme)
|
||||
skipItems(2)
|
||||
assertEquals(MainEvent.NavigateToLocalNetworkAccess, awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `cookie acquisition should not emit event when conditions are false`() =
|
||||
runTest {
|
||||
mutableCookieAcquisitionRequestFlow.value = null
|
||||
val viewModel = createViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
// Skip init events (appLanguage + appTheme)
|
||||
skipItems(2)
|
||||
mutableUserStateFlow.value = DEFAULT_USER_STATE
|
||||
expectNoEvents()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on handleResizeHasBeenRequested should set hasResizeBeenRequested as true`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
@@ -1324,12 +1390,31 @@ class MainViewModelTest : BaseViewModelTest() {
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `on HasShownAccessibilityDisclaimerUpdate with false should show the accessibility disclosure`() =
|
||||
runTest {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
// We skip the first 2 events because they are the default appTheme and appLanguage
|
||||
skipItems(2)
|
||||
|
||||
mutableHasShownAccessibilityDisclaimerFlow.value = false
|
||||
assertEquals(MainEvent.NavigateToAccessibilityDisclosure, awaitItem())
|
||||
|
||||
mutableHasShownAccessibilityDisclaimerFlow.value = true
|
||||
expectNoEvents()
|
||||
}
|
||||
}
|
||||
|
||||
private fun createViewModel(
|
||||
initialSpecialCircumstance: SpecialCircumstance? = null,
|
||||
): MainViewModel = MainViewModel(
|
||||
accessibilitySelectionManager = accessibilitySelectionManager,
|
||||
addTotpItemFromAuthenticatorManager = addTotpItemAuthenticatorManager,
|
||||
autofillSelectionManager = autofillSelectionManager,
|
||||
cookieAcquisitionRequestManager = cookieAcquisitionRequestManager,
|
||||
networkPermissionManager = networkPermissionManager,
|
||||
specialCircumstanceManager = specialCircumstanceManager,
|
||||
garbageCollectionManager = garbageCollectionManager,
|
||||
credentialProviderRequestManager = credentialProviderRequestManager,
|
||||
@@ -1360,6 +1445,7 @@ private val DEFAULT_FIRST_TIME_STATE = FirstTimeState(
|
||||
|
||||
private const val SPECIAL_CIRCUMSTANCE_KEY: String = "special-circumstance"
|
||||
private const val ACTIVE_USER_ID: String = "activeUserId"
|
||||
private const val DEFAULT_US_WEB_VAULT_URL: String = "https://vault.bitwarden.com"
|
||||
private val DEFAULT_ACCOUNT = UserState.Account(
|
||||
userId = ACTIVE_USER_ID,
|
||||
name = "Active User",
|
||||
|
||||
@@ -5795,6 +5795,9 @@ class AuthRepositoryTest {
|
||||
userId = profile.userId,
|
||||
)
|
||||
} returns resetPasswordKey.asSuccess()
|
||||
coEvery {
|
||||
vaultRepository.unlockVaultWithMasterPassword(password)
|
||||
} returns VaultUnlockResult.Success
|
||||
|
||||
val result = repository.setPassword(
|
||||
organizationIdentifier = organizationIdentifier,
|
||||
@@ -5819,6 +5822,7 @@ class AuthRepositoryTest {
|
||||
passwordHash = passwordHash,
|
||||
resetPasswordKey = resetPasswordKey,
|
||||
)
|
||||
vaultRepository.unlockVaultWithMasterPassword(password)
|
||||
vaultSdkSource.getResetPasswordKey(
|
||||
orgPublicKey = publicOrgKey,
|
||||
userId = profile.userId,
|
||||
@@ -7477,7 +7481,7 @@ class AuthRepositoryTest {
|
||||
masterPasswordUnlock = MasterPasswordUnlockDataJson(
|
||||
kdf = BASE_PROFILE_1.toSdkParams().toKdfRequestModel(),
|
||||
masterKeyWrappedUserKey = ENCRYPTED_USER_KEY,
|
||||
salt = EMAIL,
|
||||
salt = "mockSalt",
|
||||
),
|
||||
),
|
||||
)
|
||||
@@ -7529,7 +7533,7 @@ class AuthRepositoryTest {
|
||||
masterPasswordUnlock = MasterPasswordUnlockDataJson(
|
||||
kdf = BASE_PROFILE_1.toSdkParams().toKdfRequestModel(),
|
||||
masterKeyWrappedUserKey = ENCRYPTED_USER_KEY,
|
||||
salt = EMAIL,
|
||||
salt = "mockSalt",
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -9,13 +9,10 @@ import com.bitwarden.network.model.KdfTypeJson
|
||||
import com.bitwarden.network.model.KeyConnectorUserDecryptionOptionsJson
|
||||
import com.bitwarden.network.model.MasterPasswordUnlockDataJson
|
||||
import com.bitwarden.network.model.OrganizationType
|
||||
import com.bitwarden.network.model.SyncResponseJson
|
||||
import com.bitwarden.network.model.TrustedDeviceUserDecryptionOptionsJson
|
||||
import com.bitwarden.network.model.UserDecryptionJson
|
||||
import com.bitwarden.network.model.UserDecryptionOptionsJson
|
||||
import com.bitwarden.network.model.createMockOrganizationNetwork
|
||||
import com.bitwarden.network.model.createMockPermissions
|
||||
import com.bitwarden.network.model.createMockProfile
|
||||
import com.bitwarden.network.model.createMockSyncResponse
|
||||
import com.bitwarden.policies.PolicyType
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountTokensJson
|
||||
@@ -237,36 +234,36 @@ class UserStateJsonExtensionsTest {
|
||||
)
|
||||
|
||||
val orgOnlyResult = originalState.toUpdatedUserStateJson(
|
||||
syncResponse = createMockSyncResponse(
|
||||
number = 1,
|
||||
profile = createMockProfile(
|
||||
number = 1,
|
||||
id = "activeUserId",
|
||||
avatarColor = "color",
|
||||
securityStamp = "stamp",
|
||||
isPremium = false,
|
||||
isPremiumFromOrganization = true,
|
||||
),
|
||||
userDecryption = null,
|
||||
),
|
||||
syncResponse = mockk {
|
||||
every { profile } returns mockk {
|
||||
every { id } returns "activeUserId"
|
||||
every { avatarColor } returns "color"
|
||||
every { securityStamp } returns "stamp"
|
||||
every { isPremium } returns false
|
||||
every { isPremiumFromOrganization } returns true
|
||||
every { isTwoFactorEnabled } returns false
|
||||
every { creationDate } returns Instant.parse("2024-09-13T01:00:00.00Z")
|
||||
every { userDecryption } returns null
|
||||
}
|
||||
},
|
||||
)
|
||||
val orgOnlyProfile = orgOnlyResult.accounts.getValue("activeUserId").profile
|
||||
assertEquals(false, orgOnlyProfile.hasPremiumPersonally)
|
||||
assertEquals(true, orgOnlyProfile.hasPremiumFromOrganization)
|
||||
|
||||
val personalOnlyResult = originalState.toUpdatedUserStateJson(
|
||||
syncResponse = createMockSyncResponse(
|
||||
number = 1,
|
||||
profile = createMockProfile(
|
||||
number = 1,
|
||||
id = "activeUserId",
|
||||
avatarColor = "color",
|
||||
securityStamp = "stamp",
|
||||
isPremium = true,
|
||||
isPremiumFromOrganization = false,
|
||||
),
|
||||
userDecryption = null,
|
||||
),
|
||||
syncResponse = mockk {
|
||||
every { profile } returns mockk {
|
||||
every { id } returns "activeUserId"
|
||||
every { avatarColor } returns "color"
|
||||
every { securityStamp } returns "stamp"
|
||||
every { isPremium } returns true
|
||||
every { isPremiumFromOrganization } returns false
|
||||
every { isTwoFactorEnabled } returns false
|
||||
every { creationDate } returns Instant.parse("2024-09-13T01:00:00.00Z")
|
||||
every { userDecryption } returns null
|
||||
}
|
||||
},
|
||||
)
|
||||
val personalOnlyProfile = personalOnlyResult.accounts.getValue("activeUserId").profile
|
||||
assertEquals(true, personalOnlyProfile.hasPremiumPersonally)
|
||||
@@ -401,20 +398,74 @@ class UserStateJsonExtensionsTest {
|
||||
),
|
||||
)
|
||||
.toUpdatedUserStateJson(
|
||||
syncResponse = createMockSyncResponse(
|
||||
number = 1,
|
||||
profile = createMockProfile(
|
||||
number = 1,
|
||||
id = "activeUserId",
|
||||
avatarColor = "avatarColor",
|
||||
securityStamp = "securityStamp",
|
||||
isPremium = true,
|
||||
isPremiumFromOrganization = true,
|
||||
creationDate = Instant.parse("2024-09-13T01:00:00.00Z"),
|
||||
syncResponse = mockk {
|
||||
every { profile } returns mockk {
|
||||
every { id } returns "activeUserId"
|
||||
every { avatarColor } returns "avatarColor"
|
||||
every { securityStamp } returns "securityStamp"
|
||||
every { isPremium } returns true
|
||||
every { isPremiumFromOrganization } returns true
|
||||
every { isTwoFactorEnabled } returns false
|
||||
every { creationDate } returns Instant.parse("2024-09-13T01:00:00.00Z")
|
||||
every { userDecryption } returns null
|
||||
}
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `toUserStateJsonWithPassword should update active account to set hasMasterPassword and clear forcePasswordResetReason`() {
|
||||
val originalProfile = AccountJson.Profile(
|
||||
userId = "activeUserId",
|
||||
email = "email",
|
||||
isEmailVerified = true,
|
||||
name = "name",
|
||||
stamp = null,
|
||||
organizationId = null,
|
||||
avatarColorHex = null,
|
||||
hasPremiumPersonally = true,
|
||||
hasPremiumFromOrganization = null,
|
||||
forcePasswordResetReason = ForcePasswordResetReason
|
||||
.TDE_USER_WITHOUT_PASSWORD_HAS_PASSWORD_RESET_PERMISSION,
|
||||
kdfType = KdfTypeJson.ARGON2_ID,
|
||||
kdfIterations = 600000,
|
||||
kdfMemory = 16,
|
||||
kdfParallelism = 4,
|
||||
userDecryptionOptions = null,
|
||||
isTwoFactorEnabled = false,
|
||||
creationDate = Instant.parse("2024-09-13T01:00:00.00Z"),
|
||||
)
|
||||
val originalAccount = AccountJson(
|
||||
profile = originalProfile,
|
||||
tokens = mockk(),
|
||||
settings = mockk(),
|
||||
)
|
||||
assertEquals(
|
||||
UserStateJson(
|
||||
activeUserId = "activeUserId",
|
||||
accounts = mapOf(
|
||||
"activeUserId" to originalAccount.copy(
|
||||
profile = originalProfile.copy(
|
||||
forcePasswordResetReason = null,
|
||||
userDecryptionOptions = UserDecryptionOptionsJson(
|
||||
hasMasterPassword = true,
|
||||
keyConnectorUserDecryptionOptions = null,
|
||||
trustedDeviceUserDecryptionOptions = null,
|
||||
masterPasswordUnlock = null,
|
||||
),
|
||||
),
|
||||
userDecryption = null,
|
||||
),
|
||||
),
|
||||
),
|
||||
UserStateJson(
|
||||
activeUserId = "activeUserId",
|
||||
accounts = mapOf(
|
||||
"activeUserId" to originalAccount,
|
||||
),
|
||||
)
|
||||
.toUserStateJsonWithPassword(masterPasswordUnlock = null),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -485,6 +536,71 @@ class UserStateJsonExtensionsTest {
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toUserStateJsonWithPassword should preserve values of userDecryptionOptions`() {
|
||||
val keyConnectorOptionsJson = KeyConnectorUserDecryptionOptionsJson("key")
|
||||
val trustedDeviceOptionsJson = TrustedDeviceUserDecryptionOptionsJson(
|
||||
encryptedPrivateKey = "encryptedPrivateKey",
|
||||
encryptedUserKey = "encryptedUserKey",
|
||||
hasAdminApproval = true,
|
||||
hasLoginApprovingDevice = true,
|
||||
hasManageResetPasswordPermission = true,
|
||||
)
|
||||
val originalProfile = AccountJson.Profile(
|
||||
userId = "activeUserId",
|
||||
email = "email",
|
||||
isEmailVerified = true,
|
||||
name = "name",
|
||||
stamp = null,
|
||||
organizationId = null,
|
||||
avatarColorHex = null,
|
||||
hasPremiumPersonally = true,
|
||||
hasPremiumFromOrganization = null,
|
||||
forcePasswordResetReason = null,
|
||||
kdfType = KdfTypeJson.ARGON2_ID,
|
||||
kdfIterations = 600000,
|
||||
kdfMemory = 16,
|
||||
kdfParallelism = 4,
|
||||
userDecryptionOptions = UserDecryptionOptionsJson(
|
||||
hasMasterPassword = true,
|
||||
keyConnectorUserDecryptionOptions = keyConnectorOptionsJson,
|
||||
trustedDeviceUserDecryptionOptions = trustedDeviceOptionsJson,
|
||||
masterPasswordUnlock = null,
|
||||
),
|
||||
isTwoFactorEnabled = false,
|
||||
creationDate = Instant.parse("2024-09-13T01:00:00.00Z"),
|
||||
)
|
||||
val originalAccount = AccountJson(
|
||||
profile = originalProfile,
|
||||
tokens = mockk(),
|
||||
settings = mockk(),
|
||||
)
|
||||
assertEquals(
|
||||
UserStateJson(
|
||||
activeUserId = "activeUserId",
|
||||
accounts = mapOf(
|
||||
"activeUserId" to originalAccount.copy(
|
||||
profile = originalProfile.copy(
|
||||
userDecryptionOptions = UserDecryptionOptionsJson(
|
||||
hasMasterPassword = true,
|
||||
keyConnectorUserDecryptionOptions = keyConnectorOptionsJson,
|
||||
trustedDeviceUserDecryptionOptions = trustedDeviceOptionsJson,
|
||||
masterPasswordUnlock = null,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
UserStateJson(
|
||||
activeUserId = "activeUserId",
|
||||
accounts = mapOf(
|
||||
"activeUserId" to originalAccount,
|
||||
),
|
||||
)
|
||||
.toUserStateJsonWithPassword(masterPasswordUnlock = null),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toUserState should return the correct UserState for an unlocked vault`() {
|
||||
val expectedCreationDate = Instant.parse("2024-06-15T10:30:00Z")
|
||||
@@ -1877,22 +1993,20 @@ class UserStateJsonExtensionsTest {
|
||||
accounts = mapOf("activeUserId" to originalAccount),
|
||||
)
|
||||
|
||||
val syncResponse = createMockSyncResponse(
|
||||
number = 1,
|
||||
profile = createMockProfile(
|
||||
number = 1,
|
||||
id = "activeUserId",
|
||||
avatarColor = "avatarColor",
|
||||
securityStamp = "securityStamp",
|
||||
isPremium = false,
|
||||
isPremiumFromOrganization = false,
|
||||
isTwoFactorEnabled = true,
|
||||
creationDate = Instant.parse("2024-09-13T01:00:00.00Z"),
|
||||
),
|
||||
userDecryption = UserDecryptionJson(
|
||||
val syncResponse = mockk<SyncResponseJson>(relaxed = true) {
|
||||
every { profile } returns mockk {
|
||||
every { id } returns "activeUserId"
|
||||
every { avatarColor } returns "avatarColor"
|
||||
every { securityStamp } returns "securityStamp"
|
||||
every { isPremium } returns false
|
||||
every { isPremiumFromOrganization } returns false
|
||||
every { isTwoFactorEnabled } returns true
|
||||
every { creationDate } returns Instant.parse("2024-09-13T01:00:00.00Z")
|
||||
}
|
||||
every { userDecryption } returns UserDecryptionJson(
|
||||
masterPasswordUnlock = MOCK_MASTER_PASSWORD_UNLOCK_DATA,
|
||||
),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
assertEquals(
|
||||
UserStateJson(
|
||||
@@ -1968,22 +2082,20 @@ class UserStateJsonExtensionsTest {
|
||||
accounts = mapOf("activeUserId" to originalAccount),
|
||||
)
|
||||
|
||||
val syncResponse = createMockSyncResponse(
|
||||
number = 1,
|
||||
profile = createMockProfile(
|
||||
number = 1,
|
||||
id = "activeUserId",
|
||||
avatarColor = "newAvatarColor",
|
||||
securityStamp = "newSecurityStamp",
|
||||
isPremium = true,
|
||||
isPremiumFromOrganization = false,
|
||||
isTwoFactorEnabled = true,
|
||||
creationDate = Instant.parse("2024-09-13T01:00:00.00Z"),
|
||||
),
|
||||
userDecryption = UserDecryptionJson(
|
||||
val syncResponse = mockk<SyncResponseJson> {
|
||||
every { profile } returns mockk {
|
||||
every { id } returns "activeUserId"
|
||||
every { avatarColor } returns "newAvatarColor"
|
||||
every { securityStamp } returns "newSecurityStamp"
|
||||
every { isPremium } returns true
|
||||
every { isPremiumFromOrganization } returns false
|
||||
every { isTwoFactorEnabled } returns true
|
||||
every { creationDate } returns Instant.parse("2024-09-13T01:00:00.00Z")
|
||||
}
|
||||
every { userDecryption } returns UserDecryptionJson(
|
||||
masterPasswordUnlock = MOCK_MASTER_PASSWORD_UNLOCK_DATA,
|
||||
),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
assertEquals(
|
||||
UserStateJson(
|
||||
@@ -2017,7 +2129,7 @@ class UserStateJsonExtensionsTest {
|
||||
|
||||
@Test
|
||||
@Suppress("MaxLineLength")
|
||||
fun `toUpdatedUserStateJson should clear hasMasterPassword and masterPasswordUnlock when syncResponse has no userDecryption`() {
|
||||
fun `toUpdatedUserStateJson should update existing UserDecryptionOptionsJson when syncResponse has no userDecryption`() {
|
||||
val keyConnectorOptions = KeyConnectorUserDecryptionOptionsJson("keyConnectorUrl")
|
||||
val originalProfile = AccountJson.Profile(
|
||||
userId = "activeUserId",
|
||||
@@ -2053,20 +2165,18 @@ class UserStateJsonExtensionsTest {
|
||||
accounts = mapOf("activeUserId" to originalAccount),
|
||||
)
|
||||
|
||||
val syncResponse = createMockSyncResponse(
|
||||
number = 1,
|
||||
profile = createMockProfile(
|
||||
number = 1,
|
||||
id = "activeUserId",
|
||||
avatarColor = "updatedAvatarColor",
|
||||
securityStamp = "updatedSecurityStamp",
|
||||
isPremium = false,
|
||||
isPremiumFromOrganization = true,
|
||||
creationDate = Instant.parse("2024-09-13T01:00:00.00Z"),
|
||||
organizations = emptyList(),
|
||||
),
|
||||
userDecryption = null,
|
||||
)
|
||||
val syncResponse = mockk<SyncResponseJson> {
|
||||
every { profile } returns mockk {
|
||||
every { id } returns "activeUserId"
|
||||
every { avatarColor } returns "updatedAvatarColor"
|
||||
every { securityStamp } returns "updatedSecurityStamp"
|
||||
every { isPremium } returns false
|
||||
every { isPremiumFromOrganization } returns true
|
||||
every { isTwoFactorEnabled } returns false
|
||||
every { creationDate } returns Instant.parse("2024-09-13T01:00:00.00Z")
|
||||
}
|
||||
every { userDecryption } returns null
|
||||
}
|
||||
|
||||
assertEquals(
|
||||
UserStateJson(
|
||||
@@ -2081,7 +2191,7 @@ class UserStateJsonExtensionsTest {
|
||||
isTwoFactorEnabled = false,
|
||||
creationDate = Instant.parse("2024-09-13T01:00:00.00Z"),
|
||||
userDecryptionOptions = UserDecryptionOptionsJson(
|
||||
hasMasterPassword = false,
|
||||
hasMasterPassword = true,
|
||||
trustedDeviceUserDecryptionOptions = null,
|
||||
keyConnectorUserDecryptionOptions = keyConnectorOptions,
|
||||
masterPasswordUnlock = null,
|
||||
@@ -2126,28 +2236,31 @@ class UserStateJsonExtensionsTest {
|
||||
accounts = mapOf("activeUserId" to originalAccount),
|
||||
)
|
||||
|
||||
val syncResponse = createMockSyncResponse(
|
||||
number = 1,
|
||||
profile = createMockProfile(
|
||||
number = 1,
|
||||
id = "activeUserId",
|
||||
avatarColor = null,
|
||||
securityStamp = null,
|
||||
creationDate = Instant.parse("2024-09-13T01:00:00.00Z"),
|
||||
),
|
||||
userDecryption = UserDecryptionJson(
|
||||
masterPasswordUnlock = MasterPasswordUnlockDataJson(
|
||||
salt = "mockSalt",
|
||||
kdf = KdfJson(
|
||||
kdfType = KdfTypeJson.PBKDF2_SHA256,
|
||||
iterations = DEFAULT_PBKDF2_ITERATIONS,
|
||||
memory = null,
|
||||
parallelism = null,
|
||||
),
|
||||
masterKeyWrappedUserKey = "mockMasterKeyWrappedUserKey",
|
||||
),
|
||||
),
|
||||
)
|
||||
val syncResponse = mockk<SyncResponseJson> {
|
||||
every { profile } returns mockk {
|
||||
every { id } returns "activeUserId"
|
||||
every { avatarColor } returns null
|
||||
every { securityStamp } returns null
|
||||
every { isPremium } returns false
|
||||
every { isPremiumFromOrganization } returns false
|
||||
every { isTwoFactorEnabled } returns false
|
||||
every { creationDate } returns Instant.parse("2024-09-13T01:00:00.00Z")
|
||||
}
|
||||
val updatedKdf = KdfJson(
|
||||
kdfType = KdfTypeJson.PBKDF2_SHA256,
|
||||
iterations = DEFAULT_PBKDF2_ITERATIONS,
|
||||
memory = null,
|
||||
parallelism = null,
|
||||
)
|
||||
val updatedMasterPasswordUnlock = MasterPasswordUnlockDataJson(
|
||||
salt = "mockSalt",
|
||||
kdf = updatedKdf,
|
||||
masterKeyWrappedUserKey = "mockMasterKeyWrappedUserKey",
|
||||
)
|
||||
every { userDecryption } returns UserDecryptionJson(
|
||||
masterPasswordUnlock = updatedMasterPasswordUnlock,
|
||||
)
|
||||
}
|
||||
|
||||
assertEquals(
|
||||
UserStateJson(
|
||||
@@ -2180,137 +2293,6 @@ class UserStateJsonExtensionsTest {
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Suppress("MaxLineLength")
|
||||
fun `toUpdatedUserStateJson should set forcePasswordResetReason when user without master password is an organization admin`() {
|
||||
val originalUserState = createUserStateWithDecryptionOptions(
|
||||
userDecryptionOptions = TDE_USER_DECRYPTION_OPTIONS,
|
||||
)
|
||||
|
||||
val result = originalUserState.toUpdatedUserStateJson(
|
||||
syncResponse = createMockSyncResponse(
|
||||
number = 1,
|
||||
profile = createMockProfile(
|
||||
number = 1,
|
||||
id = "activeUserId",
|
||||
organizations = listOf(
|
||||
createMockOrganizationNetwork(
|
||||
number = 1,
|
||||
type = OrganizationType.ADMIN,
|
||||
),
|
||||
),
|
||||
),
|
||||
userDecryption = null,
|
||||
),
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
ForcePasswordResetReason.TDE_USER_WITHOUT_PASSWORD_HAS_PASSWORD_RESET_PERMISSION,
|
||||
result.accounts.getValue("activeUserId").profile.forcePasswordResetReason,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Suppress("MaxLineLength")
|
||||
fun `toUpdatedUserStateJson should set forcePasswordResetReason when user without master password has reset password permission`() {
|
||||
val originalUserState = createUserStateWithDecryptionOptions(
|
||||
userDecryptionOptions = TDE_USER_DECRYPTION_OPTIONS,
|
||||
)
|
||||
|
||||
val result = originalUserState.toUpdatedUserStateJson(
|
||||
syncResponse = createMockSyncResponse(
|
||||
number = 1,
|
||||
profile = createMockProfile(
|
||||
number = 1,
|
||||
id = "activeUserId",
|
||||
organizations = listOf(
|
||||
createMockOrganizationNetwork(
|
||||
number = 1,
|
||||
type = OrganizationType.USER,
|
||||
permissions = createMockPermissions(
|
||||
shouldManageResetPassword = true,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
userDecryption = UserDecryptionJson(masterPasswordUnlock = null),
|
||||
),
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
ForcePasswordResetReason.TDE_USER_WITHOUT_PASSWORD_HAS_PASSWORD_RESET_PERMISSION,
|
||||
result.accounts.getValue("activeUserId").profile.forcePasswordResetReason,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Suppress("MaxLineLength")
|
||||
fun `toUpdatedUserStateJson should not set forcePasswordResetReason when user without master password lacks reset password permission`() {
|
||||
val originalUserState = createUserStateWithDecryptionOptions(
|
||||
userDecryptionOptions = TDE_USER_DECRYPTION_OPTIONS,
|
||||
)
|
||||
|
||||
val result = originalUserState.toUpdatedUserStateJson(
|
||||
syncResponse = createMockSyncResponse(
|
||||
number = 1,
|
||||
profile = createMockProfile(
|
||||
number = 1,
|
||||
id = "activeUserId",
|
||||
organizations = listOf(
|
||||
createMockOrganizationNetwork(
|
||||
number = 1,
|
||||
type = OrganizationType.USER,
|
||||
),
|
||||
),
|
||||
),
|
||||
userDecryption = null,
|
||||
),
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
null,
|
||||
result.accounts.getValue("activeUserId").profile.forcePasswordResetReason,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Suppress("MaxLineLength")
|
||||
fun `toUpdatedUserStateJson should preserve previous forcePasswordResetReason when user has a master password`() {
|
||||
val originalUserState = createUserStateWithDecryptionOptions(
|
||||
userDecryptionOptions = UserDecryptionOptionsJson(
|
||||
hasMasterPassword = true,
|
||||
trustedDeviceUserDecryptionOptions = null,
|
||||
keyConnectorUserDecryptionOptions = null,
|
||||
masterPasswordUnlock = null,
|
||||
),
|
||||
forcePasswordResetReason = ForcePasswordResetReason.WEAK_MASTER_PASSWORD_ON_LOGIN,
|
||||
)
|
||||
|
||||
val result = originalUserState.toUpdatedUserStateJson(
|
||||
syncResponse = createMockSyncResponse(
|
||||
number = 1,
|
||||
profile = createMockProfile(
|
||||
number = 1,
|
||||
id = "activeUserId",
|
||||
organizations = listOf(
|
||||
createMockOrganizationNetwork(
|
||||
number = 1,
|
||||
type = OrganizationType.OWNER,
|
||||
),
|
||||
),
|
||||
),
|
||||
userDecryption = UserDecryptionJson(
|
||||
masterPasswordUnlock = MOCK_MASTER_PASSWORD_UNLOCK_DATA,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
ForcePasswordResetReason.WEAK_MASTER_PASSWORD_ON_LOGIN,
|
||||
result.accounts.getValue("activeUserId").profile.forcePasswordResetReason,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toUserStateJsonKdfUpdatedMinimums should update KDF settings to minimum values`() {
|
||||
val originalProfile = AccountJson.Profile(
|
||||
@@ -2512,53 +2494,3 @@ private val MOCK_MASTER_PASSWORD_UNLOCK_DATA = MasterPasswordUnlockDataJson(
|
||||
),
|
||||
masterKeyWrappedUserKey = "masterKeyWrappedUserKeyMock",
|
||||
)
|
||||
|
||||
private val TDE_USER_DECRYPTION_OPTIONS = UserDecryptionOptionsJson(
|
||||
hasMasterPassword = false,
|
||||
trustedDeviceUserDecryptionOptions = TrustedDeviceUserDecryptionOptionsJson(
|
||||
encryptedPrivateKey = "encryptedPrivateKey",
|
||||
encryptedUserKey = "encryptedUserKey",
|
||||
hasAdminApproval = true,
|
||||
hasLoginApprovingDevice = false,
|
||||
hasManageResetPasswordPermission = false,
|
||||
),
|
||||
keyConnectorUserDecryptionOptions = null,
|
||||
masterPasswordUnlock = null,
|
||||
)
|
||||
|
||||
/**
|
||||
* Creates a [UserStateJson] with a single "activeUserId" account using the given
|
||||
* [userDecryptionOptions] and [forcePasswordResetReason].
|
||||
*/
|
||||
private fun createUserStateWithDecryptionOptions(
|
||||
userDecryptionOptions: UserDecryptionOptionsJson?,
|
||||
forcePasswordResetReason: ForcePasswordResetReason? = null,
|
||||
): UserStateJson =
|
||||
UserStateJson(
|
||||
activeUserId = "activeUserId",
|
||||
accounts = mapOf(
|
||||
"activeUserId" to AccountJson(
|
||||
profile = AccountJson.Profile(
|
||||
userId = "activeUserId",
|
||||
email = "email",
|
||||
isEmailVerified = true,
|
||||
name = "name",
|
||||
stamp = null,
|
||||
organizationId = null,
|
||||
avatarColorHex = null,
|
||||
hasPremiumPersonally = true,
|
||||
hasPremiumFromOrganization = null,
|
||||
forcePasswordResetReason = forcePasswordResetReason,
|
||||
kdfType = KdfTypeJson.ARGON2_ID,
|
||||
kdfIterations = 600000,
|
||||
kdfMemory = 16,
|
||||
kdfParallelism = 4,
|
||||
userDecryptionOptions = userDecryptionOptions,
|
||||
isTwoFactorEnabled = false,
|
||||
creationDate = Instant.parse("2024-09-13T01:00:00.00Z"),
|
||||
),
|
||||
tokens = null,
|
||||
settings = AccountJson.Settings(environmentUrlData = null),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -8,7 +8,6 @@ import com.bitwarden.data.repository.model.Environment
|
||||
import com.bitwarden.network.BitwardenServiceClient
|
||||
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.data.platform.repository.EnvironmentRepository
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
@@ -30,12 +29,10 @@ class NetworkConfigManagerTest {
|
||||
unconfined = testDispatcher,
|
||||
)
|
||||
private val mutableAuthStateFlow = MutableStateFlow<AuthState>(AuthState.Uninitialized)
|
||||
private val mutableUserStateFlow = MutableStateFlow<UserState?>(null)
|
||||
private val mutableEnvironmentStateFlow = MutableStateFlow<Environment>(Environment.Us)
|
||||
|
||||
private val authRepository: AuthRepository = mockk {
|
||||
every { authStateFlow } returns mutableAuthStateFlow
|
||||
every { userStateFlow } returns mutableUserStateFlow
|
||||
}
|
||||
private val environmentRepository: EnvironmentRepository = mockk {
|
||||
every { environmentStateFlow } returns mutableEnvironmentStateFlow
|
||||
@@ -60,13 +57,10 @@ class NetworkConfigManagerTest {
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `changes in the Environment or active user ID should call getServerConfig after debounce period`() {
|
||||
mutableEnvironmentStateFlow.value = Environment.Eu
|
||||
mutableUserStateFlow.value = mockk { every { activeUserId } returns "userId" }
|
||||
fun `changes in the Environment should call getServerConfig after debounce period`() {
|
||||
mutableEnvironmentStateFlow.value = Environment.Us
|
||||
mutableUserStateFlow.value = null
|
||||
mutableEnvironmentStateFlow.value = Environment.Eu
|
||||
testDispatcher.advanceTimeByAndRunCurrent(delayTimeMillis = 500L)
|
||||
coVerify(exactly = 1) {
|
||||
serverConfigRepository.getServerConfig(forceRefresh = true)
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
package com.x8bit.bitwarden.ui.platform.feature.overlaynav
|
||||
|
||||
import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow
|
||||
import com.bitwarden.ui.platform.base.createMockNavHostController
|
||||
import com.x8bit.bitwarden.ui.platform.base.BitwardenComposeTest
|
||||
import com.x8bit.bitwarden.ui.platform.feature.accessibilitydisclosure.AccessibilityDisclosureRoute
|
||||
import com.x8bit.bitwarden.ui.platform.feature.cookieacquisition.CookieAcquisitionRoute
|
||||
import com.x8bit.bitwarden.ui.platform.feature.localnetworkaccess.LocalNetworkAccessRoute
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
|
||||
class OverlayNavScreenTest : BitwardenComposeTest() {
|
||||
private val mockNavHostController = createMockNavHostController()
|
||||
private val mutableEventFlow = bufferedMutableSharedFlow<OverlayNavEvent>()
|
||||
private val viewModel = mockk<OverlayNavViewModel> {
|
||||
every { eventFlow } returns mutableEventFlow
|
||||
every { stateFlow } returns MutableStateFlow(Unit)
|
||||
}
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
setContent {
|
||||
OverlayNavScreen(
|
||||
viewModel = viewModel,
|
||||
navController = mockNavHostController,
|
||||
onSplashScreenRemoved = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on NavigateToCookieAcquisition should navigate to the cookie acquisition screen`() {
|
||||
mutableEventFlow.tryEmit(OverlayNavEvent.NavigateToCookieAcquisition)
|
||||
composeTestRule.runOnIdle {
|
||||
verify(exactly = 1) {
|
||||
mockNavHostController.navigate(route = CookieAcquisitionRoute, builder = any())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on NavigateToLocalNetworkAccess should navigate to the local network access screen`() {
|
||||
mutableEventFlow.tryEmit(OverlayNavEvent.NavigateToLocalNetworkAccess)
|
||||
composeTestRule.runOnIdle {
|
||||
verify(exactly = 1) {
|
||||
mockNavHostController.navigate(route = LocalNetworkAccessRoute, builder = any())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `on NavigateToAccessibilityDisclosure should navigate to the accessibility disclosure screen`() {
|
||||
mutableEventFlow.tryEmit(OverlayNavEvent.NavigateToAccessibilityDisclosure)
|
||||
composeTestRule.runOnIdle {
|
||||
verify(exactly = 1) {
|
||||
mockNavHostController.navigate(
|
||||
route = AccessibilityDisclosureRoute,
|
||||
builder = any(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
package com.x8bit.bitwarden.ui.platform.feature.overlaynav
|
||||
|
||||
import app.cash.turbine.test
|
||||
import com.bitwarden.ui.platform.base.BaseViewModelTest
|
||||
import com.x8bit.bitwarden.data.platform.manager.CookieAcquisitionRequestManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.CookieAcquisitionRequest
|
||||
import com.x8bit.bitwarden.data.platform.manager.network.NetworkPermissionManager
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class OverlayNavViewModelTest : BaseViewModelTest() {
|
||||
private val mutableAccessibilityDisclaimerFlow = MutableStateFlow(true)
|
||||
private val settingsRepository: SettingsRepository = mockk {
|
||||
every { hasShownAccessibilityDisclaimerFlow } returns mutableAccessibilityDisclaimerFlow
|
||||
}
|
||||
private val mutableLocalNetworkAccessFlow = MutableStateFlow(false)
|
||||
private val networkPermissionManager: NetworkPermissionManager = mockk {
|
||||
every { isLocalNetworkAccessRequiredStateFlow } returns mutableLocalNetworkAccessFlow
|
||||
}
|
||||
private val mutableCookieAcquisitionFlow = MutableStateFlow<CookieAcquisitionRequest?>(null)
|
||||
private val cookieAcquisitionRequestManager: CookieAcquisitionRequestManager = mockk {
|
||||
every { cookieAcquisitionRequestFlow } returns mutableCookieAcquisitionFlow
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `when accessibility disclaimer flow is false should emit NavigateToAccessibilityDisclosure`() =
|
||||
runTest {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
expectNoEvents()
|
||||
mutableAccessibilityDisclaimerFlow.value = false
|
||||
assertEquals(OverlayNavEvent.NavigateToAccessibilityDisclosure, awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when local network access flow is true should emit NavigateToLocalNetworkAccess`() =
|
||||
runTest {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
expectNoEvents()
|
||||
mutableLocalNetworkAccessFlow.value = true
|
||||
assertEquals(OverlayNavEvent.NavigateToLocalNetworkAccess, awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when a cookie acquisition request is ready should emit NavigateToCookieAcquisition`() =
|
||||
runTest {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
expectNoEvents()
|
||||
mutableCookieAcquisitionFlow.value = CookieAcquisitionRequest(
|
||||
hostname = "vault.bitwarden.com",
|
||||
)
|
||||
assertEquals(OverlayNavEvent.NavigateToCookieAcquisition, awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
private fun createViewModel(): OverlayNavViewModel = OverlayNavViewModel(
|
||||
cookieAcquisitionRequestManager = cookieAcquisitionRequestManager,
|
||||
networkPermissionManager = networkPermissionManager,
|
||||
settingsRepository = settingsRepository,
|
||||
)
|
||||
}
|
||||
@@ -1571,7 +1571,7 @@ class AccountSecurityScreenTest : BitwardenComposeTest() {
|
||||
.assert(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
composeTestRule
|
||||
.onNodeWithText(text = "fingerprint-placeholder", substring = true)
|
||||
.onNodeWithText("fingerprint-placeholder")
|
||||
.assert(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ androidxRoom = "2.8.4"
|
||||
androidxSecurityCrypto = "1.1.0"
|
||||
androidxSplash = "1.2.0"
|
||||
androidxWork = "2.11.2"
|
||||
bitwardenSdk = "3.0.0-7338-5bdc976f"
|
||||
bitwardenSdk = "3.0.0-7395-1e454441"
|
||||
crashlytics = "3.0.7"
|
||||
detekt = "1.23.8"
|
||||
firebaseBom = "34.14.0"
|
||||
|
||||
@@ -121,7 +121,7 @@ internal class BitwardenServiceClientImpl(
|
||||
|
||||
override val configService: ConfigService by lazy {
|
||||
ConfigServiceImpl(
|
||||
configApi = retrofits.authenticatedApiRetrofit.create(),
|
||||
configApi = retrofits.unauthenticatedApiRetrofit.create(),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ import java.time.Clock
|
||||
import java.time.Instant
|
||||
import java.time.temporal.ChronoUnit
|
||||
|
||||
private const val MISSING_TOKEN_MESSAGE: String = "Auth token is missing!"
|
||||
private const val MISSING_PROVIDER_MESSAGE: String = "Refresh token provider is missing!"
|
||||
private const val EXPIRATION_OFFSET_MINUTES: Long = 5L
|
||||
|
||||
@@ -82,10 +83,8 @@ internal class AuthTokenManager(
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val token = getAccessToken() ?: run {
|
||||
Timber.w("Auth token is missing! Proceeding without token.")
|
||||
return chain.proceed(chain.request())
|
||||
}
|
||||
val token = getAccessToken()
|
||||
?: throw IOException(IllegalStateException(MISSING_TOKEN_MESSAGE))
|
||||
val request = chain
|
||||
.request()
|
||||
.newBuilder()
|
||||
|
||||
@@ -270,19 +270,16 @@ class AuthTokenManagerTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `intercept should proceed without token when an auth token data is missing`() {
|
||||
val token = "token"
|
||||
authTokenManager.refreshTokenProvider = object : RefreshTokenProvider {
|
||||
override fun refreshAccessTokenSynchronously(
|
||||
userId: String,
|
||||
): Result<String> = token.asSuccess()
|
||||
fun `intercept should throw an exception when an auth token data is missing`() {
|
||||
val throwable = assertThrows(IOException::class.java) {
|
||||
authTokenManager.intercept(
|
||||
chain = FakeInterceptorChain(request = request),
|
||||
)
|
||||
}
|
||||
every { mockAuthTokenProvider.getAuthTokenDataOrNull() } returns null
|
||||
|
||||
val response = authTokenManager.intercept(
|
||||
chain = FakeInterceptorChain(request = request),
|
||||
assertEquals(
|
||||
"Auth token is missing!",
|
||||
throwable.cause?.message,
|
||||
)
|
||||
assertNull(response.request.header("Authorization"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,6 @@ fun createMockNavHostController(): NavHostController =
|
||||
every { setViewModelStore(viewModelStore = any()) } just runs
|
||||
every { setLifecycleOwner(owner = any()) } just runs
|
||||
every { navigate(route = any<Any>(), navOptions = any()) } just runs
|
||||
every { navigate(route = any<Any>(), builder = any()) } just runs
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user