mirror of
https://github.com/bitwarden/android.git
synced 2026-03-09 03:33:36 -05:00
[PM-32122] Add cookie acquisition navigation (#6529)
Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
This commit is contained in:
@@ -3,6 +3,7 @@ package com.x8bit.bitwarden
|
||||
import android.content.Intent
|
||||
import com.bitwarden.ui.platform.base.BaseViewModel
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.getCookieCallbackResultOrNull
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.getDuoCallbackTokenResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.getSsoCallbackResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.getWebAuthResultOrNull
|
||||
@@ -28,6 +29,7 @@ class AuthCallbackViewModel @Inject constructor(
|
||||
val webAuthResult = action.intent.getWebAuthResultOrNull()
|
||||
val duoCallbackTokenResult = action.intent.getDuoCallbackTokenResult()
|
||||
val ssoCallbackResult = action.intent.getSsoCallbackResult()
|
||||
val cookieCallbackResult = action.intent.getCookieCallbackResultOrNull()
|
||||
when {
|
||||
yubiKeyResult != null -> {
|
||||
authRepository.setYubiKeyResult(yubiKeyResult = yubiKeyResult)
|
||||
@@ -45,6 +47,12 @@ class AuthCallbackViewModel @Inject constructor(
|
||||
)
|
||||
}
|
||||
|
||||
cookieCallbackResult != null -> {
|
||||
authRepository.setCookieCallbackResult(
|
||||
result = cookieCallbackResult,
|
||||
)
|
||||
}
|
||||
|
||||
webAuthResult != null -> {
|
||||
authRepository.setWebAuthResult(webAuthResult = webAuthResult)
|
||||
}
|
||||
|
||||
@@ -35,6 +35,8 @@ 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.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
|
||||
@@ -125,14 +127,19 @@ class MainActivity : AppCompatActivity() {
|
||||
modifier = Modifier
|
||||
.background(color = BitwardenTheme.colorScheme.background.primary),
|
||||
) {
|
||||
// Both root navigation and debug menu exist at this top level.
|
||||
// The debug menu can appear on top of the rest of the app without
|
||||
// interacting with the state-based navigation used by RootNavScreen.
|
||||
// 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 },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -206,6 +213,8 @@ class MainActivity : AppCompatActivity() {
|
||||
is MainEvent.CompleteAutofill -> handleCompleteAutofill(event)
|
||||
MainEvent.Recreate -> handleRecreate()
|
||||
MainEvent.NavigateToDebugMenu -> navController.navigateToDebugMenuScreen()
|
||||
MainEvent.NavigateToCookieAcquisition -> navController.navigateToCookieAcquisition()
|
||||
|
||||
is MainEvent.UpdateAppLocale -> {
|
||||
AppCompatDelegate.setApplicationLocales(
|
||||
LocaleListCompat.forLanguageTags(event.localeName),
|
||||
|
||||
@@ -8,6 +8,7 @@ import androidx.lifecycle.viewModelScope
|
||||
import com.bitwarden.core.data.manager.toast.ToastManager
|
||||
import com.bitwarden.cxf.model.ImportCredentialsRequestData
|
||||
import com.bitwarden.cxf.util.getProviderImportCredentialsRequest
|
||||
import com.bitwarden.data.repository.util.baseWebVaultUrlOrDefault
|
||||
import com.bitwarden.ui.platform.base.BaseViewModel
|
||||
import com.bitwarden.ui.platform.feature.settings.appearance.model.AppTheme
|
||||
import com.bitwarden.ui.platform.manager.share.ShareManager
|
||||
@@ -29,6 +30,7 @@ import com.x8bit.bitwarden.data.autofill.util.getAutofillSelectionDataOrNull
|
||||
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
|
||||
@@ -39,7 +41,6 @@ import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
import com.x8bit.bitwarden.data.platform.util.isAddTotpLoginItemFromAuthenticator
|
||||
import com.x8bit.bitwarden.data.vault.manager.model.VaultStateEvent
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import com.x8bit.bitwarden.ui.platform.feature.rootnav.RootNavViewModel
|
||||
import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.model.AppLanguage
|
||||
import com.x8bit.bitwarden.ui.platform.model.FeatureFlagsState
|
||||
import com.x8bit.bitwarden.ui.platform.util.isAccountSecurityShortcut
|
||||
@@ -48,6 +49,7 @@ import com.x8bit.bitwarden.ui.platform.util.isPasswordGeneratorShortcut
|
||||
import com.x8bit.bitwarden.ui.vault.util.getTotpDataOrNull
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.drop
|
||||
@@ -75,6 +77,7 @@ private const val ANIMATION_DEBOUNCE_DELAY_MS = 500L
|
||||
class MainViewModel @Inject constructor(
|
||||
accessibilitySelectionManager: AccessibilitySelectionManager,
|
||||
autofillSelectionManager: AutofillSelectionManager,
|
||||
cookieAcquisitionRequestManager: CookieAcquisitionRequestManager,
|
||||
private val addTotpItemFromAuthenticatorManager: AddTotpItemFromAuthenticatorManager,
|
||||
private val specialCircumstanceManager: SpecialCircumstanceManager,
|
||||
private val garbageCollectionManager: GarbageCollectionManager,
|
||||
@@ -162,6 +165,23 @@ class MainViewModel @Inject constructor(
|
||||
.onEach(::sendAction)
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
combine(
|
||||
authRepository.userStateFlow,
|
||||
cookieAcquisitionRequestManager.cookieAcquisitionRequestFlow,
|
||||
) { userState, request ->
|
||||
userState != null &&
|
||||
userState.activeAccount.isVaultUnlocked &&
|
||||
request != null &&
|
||||
request.hostname ==
|
||||
userState.activeAccount.environment.environmentUrlData
|
||||
.baseWebVaultUrlOrDefault
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
.filter { it }
|
||||
.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 {
|
||||
@@ -207,6 +227,7 @@ 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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -271,6 +292,10 @@ class MainViewModel @Inject constructor(
|
||||
mutableStateFlow.update { it.copy(isDynamicColorsEnabled = action.isDynamicColorsEnabled) }
|
||||
}
|
||||
|
||||
private fun handleCookieAcquisitionReady() {
|
||||
sendEvent(MainEvent.NavigateToCookieAcquisition)
|
||||
}
|
||||
|
||||
private fun handleFirstIntentReceived(action: MainAction.ReceiveFirstIntent) {
|
||||
handleIntent(
|
||||
intent = action.intent,
|
||||
@@ -589,6 +614,12 @@ sealed class MainAction {
|
||||
data class DynamicColorsUpdate(
|
||||
val isDynamicColorsEnabled: Boolean,
|
||||
) : Internal()
|
||||
|
||||
/**
|
||||
* Indicates that the cookie acquisition conditions are met and navigation
|
||||
* should proceed.
|
||||
*/
|
||||
data object CookieAcquisitionReady : Internal()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -618,6 +649,11 @@ sealed class MainEvent {
|
||||
*/
|
||||
data object NavigateToDebugMenu : MainEvent()
|
||||
|
||||
/**
|
||||
* Navigate to the cookie acquisition screen.
|
||||
*/
|
||||
data object NavigateToCookieAcquisition : MainEvent()
|
||||
|
||||
/**
|
||||
* Indicates that the app language has been updated.
|
||||
*/
|
||||
|
||||
@@ -4,7 +4,6 @@ package com.x8bit.bitwarden.ui.platform.feature.cookieacquisition
|
||||
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.NavOptions
|
||||
import com.bitwarden.annotation.OmitFromCoverage
|
||||
import com.bitwarden.ui.platform.base.util.composableWithSlideTransitions
|
||||
import kotlinx.serialization.Serializable
|
||||
@@ -19,20 +18,23 @@ data object CookieAcquisitionRoute
|
||||
/**
|
||||
* Add the cookie acquisition screen to the nav graph.
|
||||
*/
|
||||
fun NavGraphBuilder.cookieAcquisitionDestination() {
|
||||
fun NavGraphBuilder.cookieAcquisitionDestination(
|
||||
onDismiss: () -> Unit,
|
||||
onSplashScreenRemoved: () -> Unit,
|
||||
) {
|
||||
composableWithSlideTransitions<CookieAcquisitionRoute> {
|
||||
CookieAcquisitionScreen()
|
||||
CookieAcquisitionScreen(onDismiss = onDismiss)
|
||||
// If we are displaying the cookie acquisition screen, then we can just hide
|
||||
// the splash screen.
|
||||
onSplashScreenRemoved()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to the cookie acquisition screen.
|
||||
*/
|
||||
fun NavController.navigateToCookieAcquisition(
|
||||
navOptions: NavOptions? = null,
|
||||
) {
|
||||
this.navigate(
|
||||
route = CookieAcquisitionRoute,
|
||||
navOptions = navOptions,
|
||||
)
|
||||
fun NavController.navigateToCookieAcquisition() {
|
||||
this.navigate(route = CookieAcquisitionRoute) {
|
||||
launchSingleTop = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.x8bit.bitwarden.ui.platform.feature.cookieacquisition
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
@@ -51,6 +52,7 @@ import com.x8bit.bitwarden.ui.platform.feature.cookieacquisition.handlers.rememb
|
||||
*/
|
||||
@Composable
|
||||
fun CookieAcquisitionScreen(
|
||||
onDismiss: () -> Unit,
|
||||
viewModel: CookieAcquisitionViewModel = hiltViewModel(),
|
||||
intentManager: IntentManager = LocalIntentManager.current,
|
||||
) {
|
||||
@@ -65,11 +67,20 @@ fun CookieAcquisitionScreen(
|
||||
is CookieAcquisitionEvent.NavigateToHelp -> {
|
||||
intentManager.launchUri(event.uri.toUri())
|
||||
}
|
||||
|
||||
CookieAcquisitionEvent.NavigateBack -> onDismiss()
|
||||
}
|
||||
}
|
||||
|
||||
val handler = rememberCookieAcquisitionHandler(viewModel = viewModel)
|
||||
|
||||
// Route back through the ViewModel so the pending cookie request is cleared
|
||||
// before dismissing. A normal back-pop would leave the request active and
|
||||
// MainViewModel would immediately re-navigate to this screen.
|
||||
BackHandler {
|
||||
viewModel.trySendAction(CookieAcquisitionAction.ContinueWithoutSyncingClick)
|
||||
}
|
||||
|
||||
CookieAcquisitionDialogs(
|
||||
dialogState = state.dialogState,
|
||||
onDismissRequest = handler.onDismissDialogClick,
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.os.Parcelable
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.bitwarden.data.repository.util.baseWebVaultUrlOrDefault
|
||||
import com.bitwarden.ui.platform.base.BackgroundEvent
|
||||
import com.bitwarden.ui.platform.base.BaseViewModel
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
import com.bitwarden.ui.util.Text
|
||||
@@ -84,6 +85,7 @@ class CookieAcquisitionViewModel @Inject constructor(
|
||||
|
||||
private fun handleContinueWithoutSyncingClick() {
|
||||
cookieAcquisitionRequestManager.setPendingCookieAcquisition(data = null)
|
||||
sendEvent(CookieAcquisitionEvent.NavigateBack)
|
||||
}
|
||||
|
||||
private fun handleWhyAmISeeingThisClick() {
|
||||
@@ -100,6 +102,7 @@ class CookieAcquisitionViewModel @Inject constructor(
|
||||
when (action.result) {
|
||||
is CookieCallbackResult.Success -> {
|
||||
cookieAcquisitionRequestManager.setPendingCookieAcquisition(data = null)
|
||||
sendEvent(CookieAcquisitionEvent.NavigateBack)
|
||||
}
|
||||
|
||||
is CookieCallbackResult.MissingCookie -> {
|
||||
@@ -152,6 +155,14 @@ sealed class CookieAcquisitionEvent {
|
||||
* Navigate to the help page.
|
||||
*/
|
||||
data class NavigateToHelp(val uri: String) : CookieAcquisitionEvent()
|
||||
|
||||
/**
|
||||
* Navigate back, dismissing the cookie acquisition screen.
|
||||
*
|
||||
* Implements [BackgroundEvent] because the cookie callback result may arrive while
|
||||
* the screen is not resumed (e.g. returning from a Custom Tab browser session).
|
||||
*/
|
||||
data object NavigateBack : CookieAcquisitionEvent(), BackgroundEvent
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -125,6 +125,21 @@ fun DebugMenuScreen(
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
)
|
||||
Spacer(Modifier.height(height = 8.dp))
|
||||
BitwardenFilledButton(
|
||||
label = stringResource(BitwardenString.trigger_cookie_acquisition),
|
||||
onClick = remember(viewModel) {
|
||||
{
|
||||
viewModel.trySendAction(
|
||||
DebugMenuAction.TriggerCookieAcquisition,
|
||||
)
|
||||
}
|
||||
},
|
||||
isEnabled = true,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
)
|
||||
Spacer(Modifier.height(height = 16.dp))
|
||||
BitwardenHorizontalDivider()
|
||||
Spacer(Modifier.height(height = 16.dp))
|
||||
|
||||
@@ -2,11 +2,15 @@ package com.x8bit.bitwarden.ui.platform.feature.debugmenu
|
||||
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.bitwarden.core.data.manager.model.FlagKey
|
||||
import com.bitwarden.data.repository.util.baseWebVaultUrlOrDefault
|
||||
import com.bitwarden.ui.platform.base.BaseViewModel
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.platform.manager.CookieAcquisitionRequestManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.LogsManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.CookieAcquisitionRequest
|
||||
import com.x8bit.bitwarden.data.platform.repository.DebugMenuRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.collections.immutable.ImmutableMap
|
||||
import kotlinx.collections.immutable.persistentMapOf
|
||||
@@ -23,12 +27,15 @@ import javax.inject.Inject
|
||||
/**
|
||||
* ViewModel for the [DebugMenuScreen]
|
||||
*/
|
||||
@Suppress("TooManyFunctions")
|
||||
@HiltViewModel
|
||||
class DebugMenuViewModel @Inject constructor(
|
||||
featureFlagManager: FeatureFlagManager,
|
||||
private val debugMenuRepository: DebugMenuRepository,
|
||||
private val authRepository: AuthRepository,
|
||||
private val logsManager: LogsManager,
|
||||
private val cookieAcquisitionRequestManager: CookieAcquisitionRequestManager,
|
||||
private val environmentRepository: EnvironmentRepository,
|
||||
) : BaseViewModel<DebugMenuState, DebugMenuEvent, DebugMenuAction>(
|
||||
initialState = DebugMenuState(featureFlags = persistentMapOf()),
|
||||
) {
|
||||
@@ -56,6 +63,7 @@ class DebugMenuViewModel @Inject constructor(
|
||||
DebugMenuAction.ResetCoachMarkTourStatuses -> handleResetCoachMarkTourStatuses()
|
||||
DebugMenuAction.GenerateCrashClick -> handleCrashClick()
|
||||
DebugMenuAction.GenerateErrorReportClick -> handleErrorReportClick()
|
||||
DebugMenuAction.TriggerCookieAcquisition -> handleTriggerCookieAcquisition()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,6 +100,17 @@ class DebugMenuViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleTriggerCookieAcquisition() {
|
||||
cookieAcquisitionRequestManager.setPendingCookieAcquisition(
|
||||
data = CookieAcquisitionRequest(
|
||||
hostname = environmentRepository
|
||||
.environment
|
||||
.environmentUrlData
|
||||
.baseWebVaultUrlOrDefault,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun handleNavigateBack() {
|
||||
sendEvent(DebugMenuEvent.NavigateBack)
|
||||
}
|
||||
@@ -172,6 +191,11 @@ sealed class DebugMenuAction {
|
||||
*/
|
||||
data object GenerateErrorReportClick : DebugMenuAction()
|
||||
|
||||
/**
|
||||
* The user has clicked trigger cookie acquisition button.
|
||||
*/
|
||||
data object TriggerCookieAcquisition : DebugMenuAction()
|
||||
|
||||
/**
|
||||
* Internal actions not triggered from the UI.
|
||||
*/
|
||||
|
||||
@@ -3,9 +3,11 @@ package com.x8bit.bitwarden
|
||||
import android.content.Intent
|
||||
import com.bitwarden.ui.platform.base.BaseViewModelTest
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.CookieCallbackResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.DuoCallbackTokenResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.SsoCallbackResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.WebAuthResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.getCookieCallbackResultOrNull
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.getDuoCallbackTokenResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.getSsoCallbackResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.getWebAuthResultOrNull
|
||||
@@ -24,6 +26,7 @@ import org.junit.jupiter.api.Test
|
||||
|
||||
class AuthCallbackViewModelTest : BaseViewModelTest() {
|
||||
private val authRepository = mockk<AuthRepository> {
|
||||
every { setCookieCallbackResult(any()) } just runs
|
||||
every { setSsoCallbackResult(any()) } just runs
|
||||
every { setDuoCallbackTokenResult(any()) } just runs
|
||||
every { setYubiKeyResult(any()) } just runs
|
||||
@@ -33,6 +36,7 @@ class AuthCallbackViewModelTest : BaseViewModelTest() {
|
||||
@BeforeEach
|
||||
fun setUp() {
|
||||
mockkStatic(
|
||||
Intent::getCookieCallbackResultOrNull,
|
||||
Intent::getYubiKeyResultOrNull,
|
||||
Intent::getWebAuthResultOrNull,
|
||||
Intent::getDuoCallbackTokenResult,
|
||||
@@ -43,6 +47,7 @@ class AuthCallbackViewModelTest : BaseViewModelTest() {
|
||||
@AfterEach
|
||||
fun tearDown() {
|
||||
unmockkStatic(
|
||||
Intent::getCookieCallbackResultOrNull,
|
||||
Intent::getYubiKeyResultOrNull,
|
||||
Intent::getWebAuthResultOrNull,
|
||||
Intent::getDuoCallbackTokenResult,
|
||||
@@ -59,6 +64,7 @@ class AuthCallbackViewModelTest : BaseViewModelTest() {
|
||||
every { mockIntent.getYubiKeyResultOrNull() } returns null
|
||||
every { mockIntent.getWebAuthResultOrNull() } returns null
|
||||
every { mockIntent.getSsoCallbackResult() } returns null
|
||||
every { mockIntent.getCookieCallbackResultOrNull() } returns null
|
||||
|
||||
viewModel.trySendAction(AuthCallbackAction.IntentReceive(intent = mockIntent))
|
||||
verify(exactly = 1) {
|
||||
@@ -78,6 +84,7 @@ class AuthCallbackViewModelTest : BaseViewModelTest() {
|
||||
every { mockIntent.getYubiKeyResultOrNull() } returns null
|
||||
every { mockIntent.getWebAuthResultOrNull() } returns null
|
||||
every { mockIntent.getDuoCallbackTokenResult() } returns null
|
||||
every { mockIntent.getCookieCallbackResultOrNull() } returns null
|
||||
|
||||
viewModel.trySendAction(AuthCallbackAction.IntentReceive(intent = mockIntent))
|
||||
verify(exactly = 1) {
|
||||
@@ -94,6 +101,7 @@ class AuthCallbackViewModelTest : BaseViewModelTest() {
|
||||
every { mockIntent.getWebAuthResultOrNull() } returns null
|
||||
every { mockIntent.getDuoCallbackTokenResult() } returns null
|
||||
every { mockIntent.getSsoCallbackResult() } returns null
|
||||
every { mockIntent.getCookieCallbackResultOrNull() } returns null
|
||||
|
||||
viewModel.trySendAction(AuthCallbackAction.IntentReceive(intent = mockIntent))
|
||||
verify(exactly = 1) {
|
||||
@@ -110,6 +118,7 @@ class AuthCallbackViewModelTest : BaseViewModelTest() {
|
||||
every { getYubiKeyResultOrNull() } returns null
|
||||
every { getDuoCallbackTokenResult() } returns null
|
||||
every { getSsoCallbackResult() } returns null
|
||||
every { getCookieCallbackResultOrNull() } returns null
|
||||
}
|
||||
|
||||
viewModel.trySendAction(AuthCallbackAction.IntentReceive(intent = mockIntent))
|
||||
@@ -118,6 +127,25 @@ class AuthCallbackViewModelTest : BaseViewModelTest() {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on IntentReceive with cookie callback should call setCookieCallbackResult`() {
|
||||
val viewModel = createViewModel()
|
||||
val mockIntent = mockk<Intent>()
|
||||
val cookieCallbackResult = CookieCallbackResult.Success(
|
||||
cookies = mapOf("cookie" to "value"),
|
||||
)
|
||||
every { mockIntent.getCookieCallbackResultOrNull() } returns cookieCallbackResult
|
||||
every { mockIntent.getYubiKeyResultOrNull() } returns null
|
||||
every { mockIntent.getWebAuthResultOrNull() } returns null
|
||||
every { mockIntent.getDuoCallbackTokenResult() } returns null
|
||||
every { mockIntent.getSsoCallbackResult() } returns null
|
||||
|
||||
viewModel.trySendAction(AuthCallbackAction.IntentReceive(intent = mockIntent))
|
||||
verify(exactly = 1) {
|
||||
authRepository.setCookieCallbackResult(result = cookieCallbackResult)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createViewModel() = AuthCallbackViewModel(
|
||||
authRepository = authRepository,
|
||||
)
|
||||
|
||||
@@ -52,11 +52,13 @@ 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
|
||||
@@ -171,6 +173,11 @@ 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 credentialProviderRequestManager: CredentialProviderRequestManager = mockk {
|
||||
every { getPendingCredentialRequest() } returns null
|
||||
}
|
||||
@@ -256,14 +263,14 @@ class MainViewModelTest : BaseViewModelTest() {
|
||||
|
||||
viewModel.eventFlow.test {
|
||||
// We skip the first 2 events because they are the default appTheme and appLanguage
|
||||
awaitItem()
|
||||
awaitItem()
|
||||
skipItems(2)
|
||||
|
||||
mutableUserStateFlow.value = UserState(
|
||||
activeUserId = userId1,
|
||||
accounts = listOf(
|
||||
mockk<UserState.Account> {
|
||||
every { userId } returns userId1
|
||||
every { isVaultUnlocked } returns false
|
||||
},
|
||||
),
|
||||
hasPendingAccountAddition = false,
|
||||
@@ -275,6 +282,7 @@ class MainViewModelTest : BaseViewModelTest() {
|
||||
accounts = listOf(
|
||||
mockk<UserState.Account> {
|
||||
every { userId } returns userId1
|
||||
every { isVaultUnlocked } returns false
|
||||
},
|
||||
),
|
||||
hasPendingAccountAddition = true,
|
||||
@@ -286,9 +294,11 @@ class MainViewModelTest : BaseViewModelTest() {
|
||||
accounts = listOf(
|
||||
mockk<UserState.Account> {
|
||||
every { userId } returns userId1
|
||||
every { isVaultUnlocked } returns false
|
||||
},
|
||||
mockk<UserState.Account> {
|
||||
every { userId } returns userId2
|
||||
every { isVaultUnlocked } returns false
|
||||
},
|
||||
),
|
||||
hasPendingAccountAddition = true,
|
||||
@@ -1142,12 +1152,46 @@ 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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@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()
|
||||
}
|
||||
}
|
||||
|
||||
private fun createViewModel(
|
||||
initialSpecialCircumstance: SpecialCircumstance? = null,
|
||||
) = MainViewModel(
|
||||
accessibilitySelectionManager = accessibilitySelectionManager,
|
||||
addTotpItemFromAuthenticatorManager = addTotpItemAuthenticatorManager,
|
||||
autofillSelectionManager = autofillSelectionManager,
|
||||
cookieAcquisitionRequestManager = cookieAcquisitionRequestManager,
|
||||
specialCircumstanceManager = specialCircumstanceManager,
|
||||
garbageCollectionManager = garbageCollectionManager,
|
||||
credentialProviderRequestManager = credentialProviderRequestManager,
|
||||
@@ -1177,6 +1221,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",
|
||||
|
||||
@@ -21,11 +21,13 @@ import io.mockk.runs
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
|
||||
class CookieAcquisitionScreenTest : BitwardenComposeTest() {
|
||||
|
||||
private var onDismissCalled = false
|
||||
private val mutableEventFlow =
|
||||
bufferedMutableSharedFlow<CookieAcquisitionEvent>()
|
||||
private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE)
|
||||
@@ -44,6 +46,7 @@ class CookieAcquisitionScreenTest : BitwardenComposeTest() {
|
||||
intentManager = intentManager,
|
||||
) {
|
||||
CookieAcquisitionScreen(
|
||||
onDismiss = { onDismissCalled = true },
|
||||
viewModel = viewModel,
|
||||
)
|
||||
}
|
||||
@@ -148,6 +151,16 @@ class CookieAcquisitionScreenTest : BitwardenComposeTest() {
|
||||
.assertExists()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on system back should send ContinueWithoutSyncingClick action`() {
|
||||
backDispatcher?.onBackPressed()
|
||||
verify {
|
||||
viewModel.trySendAction(
|
||||
CookieAcquisitionAction.ContinueWithoutSyncingClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `error dialog dismiss should send DismissDialogClick action`() {
|
||||
mutableStateFlow.update {
|
||||
@@ -170,6 +183,12 @@ class CookieAcquisitionScreenTest : BitwardenComposeTest() {
|
||||
viewModel.trySendAction(CookieAcquisitionAction.DismissDialogClick)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `NavigateBack event should call onDismiss`() {
|
||||
mutableEventFlow.tryEmit(CookieAcquisitionEvent.NavigateBack)
|
||||
assertTrue(onDismissCalled)
|
||||
}
|
||||
}
|
||||
|
||||
private const val DEFAULT_ENVIRONMENT_URL = "https://vault.bitwarden.com"
|
||||
|
||||
@@ -88,6 +88,21 @@ class CookieAcquisitionViewModelTest : BaseViewModelTest() {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ContinueWithoutSyncingClick should emit NavigateBack event`() =
|
||||
runTest {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(
|
||||
CookieAcquisitionAction.ContinueWithoutSyncingClick,
|
||||
)
|
||||
assertEquals(
|
||||
CookieAcquisitionEvent.NavigateBack,
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WhyAmISeeingThisClick should emit NavigateToHelp event`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
@@ -103,14 +118,17 @@ class CookieAcquisitionViewModelTest : BaseViewModelTest() {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `CookieCallbackResult Success should clear pending request`() =
|
||||
fun `CookieCallbackResult Success should clear pending request and emit NavigateBack`() =
|
||||
runTest {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
mutableCookieCallbackResultFlow.emit(
|
||||
CookieCallbackResult.Success(cookies = mapOf("cookie" to "value")),
|
||||
)
|
||||
expectNoEvents()
|
||||
assertEquals(
|
||||
CookieAcquisitionEvent.NavigateBack,
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
verify {
|
||||
mockCookieAcquisitionRequestManager.setPendingCookieAcquisition(data = null)
|
||||
|
||||
@@ -5,9 +5,12 @@ import com.bitwarden.core.data.manager.model.FlagKey
|
||||
import com.bitwarden.core.data.util.assertCoroutineThrows
|
||||
import com.bitwarden.ui.platform.base.BaseViewModelTest
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.platform.manager.CookieAcquisitionRequestManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.LogsManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.CookieAcquisitionRequest
|
||||
import com.x8bit.bitwarden.data.platform.repository.DebugMenuRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.FakeEnvironmentRepository
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.every
|
||||
@@ -48,6 +51,13 @@ class DebugMenuViewModelTest : BaseViewModelTest() {
|
||||
every { trackNonFatalException(throwable = any()) } just runs
|
||||
}
|
||||
|
||||
private val mockCookieAcquisitionRequestManager =
|
||||
mockk<CookieAcquisitionRequestManager> {
|
||||
every { setPendingCookieAcquisition(data = any()) } just runs
|
||||
}
|
||||
|
||||
private val fakeEnvironmentRepository = FakeEnvironmentRepository()
|
||||
|
||||
@Test
|
||||
fun `initial state should be correct`() {
|
||||
val viewModel = createViewModel()
|
||||
@@ -135,11 +145,28 @@ class DebugMenuViewModelTest : BaseViewModelTest() {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `TriggerCookieAcquisition should set pending cookie acquisition`() =
|
||||
runTest {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.trySendAction(DebugMenuAction.TriggerCookieAcquisition)
|
||||
verify(exactly = 1) {
|
||||
mockCookieAcquisitionRequestManager.setPendingCookieAcquisition(
|
||||
data = CookieAcquisitionRequest(
|
||||
hostname = "https://vault.bitwarden.com",
|
||||
),
|
||||
)
|
||||
}
|
||||
viewModel.eventFlow.test { expectNoEvents() }
|
||||
}
|
||||
|
||||
private fun createViewModel(): DebugMenuViewModel = DebugMenuViewModel(
|
||||
featureFlagManager = mockFeatureFlagManager,
|
||||
debugMenuRepository = mockDebugMenuRepository,
|
||||
authRepository = mockAuthRepository,
|
||||
logsManager = logsManager,
|
||||
cookieAcquisitionRequestManager = mockCookieAcquisitionRequestManager,
|
||||
environmentRepository = fakeEnvironmentRepository,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
<string name="migrate_my_vault_to_my_items">Migrate My Vault to My Items</string>
|
||||
<string name="archive_items">Archive Items</string>
|
||||
<string name="send_email_verification">Send Email Verification</string>
|
||||
<string name="trigger_cookie_acquisition">Trigger cookie acquisition</string>
|
||||
|
||||
<!-- endregion Debug Menu -->
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user