diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryImpl.kt index afc6be8fa..edea34bde 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryImpl.kt @@ -266,7 +266,8 @@ class AuthRepositoryImpl constructor( val previousActiveUserId = currentUserState.activeUserId if (userId == previousActiveUserId) { - // Nothing to do + // No switching to do but clear any special circumstances + specialCircumstance = null return SwitchAccountResult.NoChange } @@ -281,6 +282,10 @@ class AuthRepositoryImpl constructor( // Lock and clear data for the previous user vaultRepository.lockVaultIfNecessary(previousActiveUserId) vaultRepository.clearUnlockedData() + + // Clear any special circumstances + specialCircumstance = null + return SwitchAccountResult.AccountSwitched } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/landing/LandingScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/landing/LandingScreen.kt index 1172e65fd..a391995ba 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/landing/LandingScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/landing/LandingScreen.kt @@ -27,6 +27,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi @@ -46,14 +47,18 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.x8bit.bitwarden.R import com.x8bit.bitwarden.data.platform.repository.model.Environment import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect +import com.x8bit.bitwarden.ui.platform.components.BitwardenAccountSwitcher import com.x8bit.bitwarden.ui.platform.components.BitwardenBasicDialog import com.x8bit.bitwarden.ui.platform.components.BitwardenFilledButton +import com.x8bit.bitwarden.ui.platform.components.BitwardenPlaceholderAccountActionItem import com.x8bit.bitwarden.ui.platform.components.BitwardenScaffold import com.x8bit.bitwarden.ui.platform.components.BitwardenSelectionDialog import com.x8bit.bitwarden.ui.platform.components.BitwardenSelectionRow import com.x8bit.bitwarden.ui.platform.components.BitwardenSwitch import com.x8bit.bitwarden.ui.platform.components.BitwardenTextButton import com.x8bit.bitwarden.ui.platform.components.BitwardenTextField +import com.x8bit.bitwarden.ui.platform.components.BitwardenTopAppBar +import kotlinx.collections.immutable.toImmutableList /** * The top level composable for the Landing screen. @@ -86,17 +91,34 @@ fun LandingScreen( }, ) - val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) + val isAppBarVisible = state.accountSummaries.isNotEmpty() + var isAccountMenuVisible by rememberSaveable { mutableStateOf(false) } + val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior( + state = rememberTopAppBarState(), + canScroll = { !isAccountMenuVisible }, + ) BitwardenScaffold( modifier = Modifier .fillMaxSize() .nestedScroll(scrollBehavior.nestedScrollConnection), topBar = { - // Empty + if (isAppBarVisible) { + BitwardenTopAppBar( + title = "", + scrollBehavior = scrollBehavior, + navigationIcon = null, + actions = { + BitwardenPlaceholderAccountActionItem( + onClick = { isAccountMenuVisible = !isAccountMenuVisible }, + ) + }, + ) + } }, ) { innerPadding -> LandingScreenContent( state = state, + isAppBarVisible = isAppBarVisible, onEmailInputChange = remember(viewModel) { { viewModel.trySendAction(LandingAction.EmailInputChanged(it)) } }, @@ -116,6 +138,23 @@ fun LandingScreen( .padding(innerPadding) .fillMaxSize(), ) + + BitwardenAccountSwitcher( + isVisible = isAccountMenuVisible, + accountSummaries = state.accountSummaries.toImmutableList(), + onAccountSummaryClick = remember(viewModel) { + { viewModel.trySendAction(LandingAction.SwitchAccountClick(it)) } + }, + onAddAccountClick = { + // Not available + }, + onDismissRequest = { isAccountMenuVisible = false }, + isAddAccountAvailable = false, + topAppBarScrollBehavior = scrollBehavior, + modifier = Modifier + .padding(innerPadding) + .fillMaxSize(), + ) } } @@ -124,6 +163,7 @@ fun LandingScreen( @Composable private fun LandingScreenContent( state: LandingState, + isAppBarVisible: Boolean, onEmailInputChange: (String) -> Unit, onEnvironmentTypeSelect: (Environment.Type) -> Unit, onRememberMeToggle: (Boolean) -> Unit, @@ -138,7 +178,8 @@ private fun LandingScreenContent( .imePadding() .verticalScroll(rememberScrollState()), ) { - Spacer(modifier = Modifier.height(104.dp)) + val topPadding = if (isAppBarVisible) 40.dp else 104.dp + Spacer(modifier = Modifier.height(topPadding)) Image( painter = painterResource(id = R.drawable.logo), diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/landing/LandingViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/landing/LandingViewModel.kt index ef75ec2d2..3af977050 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/landing/LandingViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/landing/LandingViewModel.kt @@ -11,6 +11,8 @@ import com.x8bit.bitwarden.ui.platform.base.BaseViewModel import com.x8bit.bitwarden.ui.platform.base.util.asText import com.x8bit.bitwarden.ui.platform.base.util.isValidEmail import com.x8bit.bitwarden.ui.platform.components.BasicDialogState +import com.x8bit.bitwarden.ui.platform.components.model.AccountSummary +import com.x8bit.bitwarden.ui.vault.feature.vault.util.toAccountSummaries import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach @@ -36,6 +38,7 @@ class LandingViewModel @Inject constructor( isRememberMeEnabled = authRepository.rememberedEmailAddress != null, selectedEnvironmentType = environmentRepository.environment.type, errorDialogState = BasicDialogState.Hidden, + accountSummaries = authRepository.userStateFlow.value?.toAccountSummaries().orEmpty(), ), ) { @@ -61,6 +64,7 @@ class LandingViewModel @Inject constructor( override fun handleAction(action: LandingAction) { when (action) { + is LandingAction.SwitchAccountClick -> handleSwitchAccountClicked(action) is LandingAction.ContinueButtonClick -> handleContinueButtonClicked() LandingAction.CreateAccountClick -> handleCreateAccountClicked() is LandingAction.ErrorDialogDismiss -> handleErrorDialogDismiss() @@ -73,6 +77,10 @@ class LandingViewModel @Inject constructor( } } + private fun handleSwitchAccountClicked(action: LandingAction.SwitchAccountClick) { + authRepository.switchAccount(userId = action.account.userId) + } + private fun handleEmailInputUpdated(action: LandingAction.EmailInputChanged) { val email = action.input mutableStateFlow.update { @@ -156,6 +164,7 @@ data class LandingState( val isRememberMeEnabled: Boolean, val selectedEnvironmentType: Environment.Type, val errorDialogState: BasicDialogState, + val accountSummaries: List<AccountSummary>, ) : Parcelable /** @@ -184,6 +193,13 @@ sealed class LandingEvent { * Models actions for the landing screen. */ sealed class LandingAction { + /** + * Indicates the user has clicked on the given [account] information in order to switch to it. + */ + data class SwitchAccountClick( + val account: AccountSummary, + ) : LandingAction() + /** * Indicates that the continue button has been clicked and the app should navigate to Login. */ diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/BitwardenAccountSwitcher.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/BitwardenAccountSwitcher.kt index 35e95c95b..569765cc7 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/BitwardenAccountSwitcher.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/BitwardenAccountSwitcher.kt @@ -66,6 +66,9 @@ private const val MAXIMUM_ACCOUNT_LIMIT = 5 * @param onAddAccountClick A callback when the Add Account row is clicked. * @param onDismissRequest A callback when the component requests to be dismissed. This is triggered * whenever the user clicks on the scrim or any of the switcher items. + * @param isAddAccountAvailable Whether or not the "Add account" button is available. Note that even + * when `true`, this button may be hidden when there are more than [MAXIMUM_ACCOUNT_LIMIT] accounts + * present. * @param modifier A [Modifier] for the composable. * @param topAppBarScrollBehavior Used to derive the background color of the content and keep it in * sync with the associated app bar. @@ -79,6 +82,7 @@ fun BitwardenAccountSwitcher( onAddAccountClick: () -> Unit, onDismissRequest: () -> Unit, modifier: Modifier = Modifier, + isAddAccountAvailable: Boolean = true, topAppBarScrollBehavior: TopAppBarScrollBehavior, ) { Box(modifier = modifier) { @@ -99,6 +103,7 @@ fun BitwardenAccountSwitcher( onDismissRequest() onAddAccountClick() }, + isAddAccountAvailable = isAddAccountAvailable, topAppBarScrollBehavior = topAppBarScrollBehavior, modifier = Modifier .fillMaxWidth(), @@ -113,6 +118,7 @@ private fun AnimatedAccountSwitcher( accountSummaries: ImmutableList<AccountSummary>, onAccountSummaryClick: (AccountSummary) -> Unit, onAddAccountClick: () -> Unit, + isAddAccountAvailable: Boolean, modifier: Modifier = Modifier, topAppBarScrollBehavior: TopAppBarScrollBehavior, ) { @@ -130,11 +136,16 @@ private fun AnimatedAccountSwitcher( .padding(bottom = 24.dp) // Match the color of the switcher the different states of the app bar. .drawBehind { + val progressFraction = if (topAppBarScrollBehavior.isPinned) { + topAppBarScrollBehavior.state.overlappedFraction + } else { + topAppBarScrollBehavior.state.collapsedFraction + } val contentBackgroundColor = lerp( start = expandedColor, stop = collapsedColor, - fraction = topAppBarScrollBehavior.state.collapsedFraction, + fraction = progressFraction, ) drawRect(contentBackgroundColor) }, @@ -154,7 +165,7 @@ private fun AnimatedAccountSwitcher( color = MaterialTheme.colorScheme.outlineVariant, ) } - if (accountSummaries.size < MAXIMUM_ACCOUNT_LIMIT) { + if (accountSummaries.size < MAXIMUM_ACCOUNT_LIMIT && isAddAccountAvailable) { item { AddAccountItem( onClick = onAddAccountClick, diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/BitwardenPlaceholderAccountItem.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/BitwardenPlaceholderAccountItem.kt new file mode 100644 index 000000000..c512359cc --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/BitwardenPlaceholderAccountItem.kt @@ -0,0 +1,60 @@ +package com.x8bit.bitwarden.ui.platform.components + +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.tooling.preview.Preview +import com.x8bit.bitwarden.R +import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme + +/** + * A placeholder item to be used to represent an account. + * + * @param onClick An action to be invoked when the icon is clicked. + */ +@Composable +fun BitwardenPlaceholderAccountActionItem( + onClick: () -> Unit, +) { + IconButton( + onClick = onClick, + modifier = Modifier + .semantics(mergeDescendants = true) {}, + ) { + Icon( + painter = painterResource(id = R.drawable.ic_account_initials_container), + contentDescription = null, + tint = MaterialTheme.colorScheme.secondaryContainer, + ) + Icon( + painter = painterResource(id = R.drawable.ic_dots), + contentDescription = stringResource(id = R.string.account), + tint = MaterialTheme.colorScheme.onSecondaryContainer, + ) + } +} + +@Preview +@Composable +private fun BitwardenPlaceholderAccountActionItem_preview_light() { + BitwardenTheme(darkTheme = false) { + BitwardenPlaceholderAccountActionItem( + onClick = {}, + ) + } +} + +@Preview +@Composable +private fun BitwardenPlaceholderAccountActionItem_preview_dark() { + BitwardenTheme(darkTheme = true) { + BitwardenPlaceholderAccountActionItem( + onClick = {}, + ) + } +} diff --git a/app/src/main/res/drawable/ic_dots.xml b/app/src/main/res/drawable/ic_dots.xml new file mode 100644 index 000000000..1108735d0 --- /dev/null +++ b/app/src/main/res/drawable/ic_dots.xml @@ -0,0 +1,12 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path + android:pathData="M13.663,12.05C13.663,11.756 13.753,11.509 13.934,11.309C14.122,11.103 14.391,11 14.741,11C15.091,11 15.356,11.103 15.538,11.309C15.725,11.509 15.819,11.756 15.819,12.05C15.819,12.337 15.725,12.581 15.538,12.781C15.356,12.981 15.091,13.081 14.741,13.081C14.391,13.081 14.122,12.981 13.934,12.781C13.753,12.581 13.663,12.337 13.663,12.05Z" + android:fillColor="#151B2C"/> + <path + android:pathData="M8,12.05C8,11.756 8.091,11.509 8.272,11.309C8.459,11.103 8.728,11 9.078,11C9.428,11 9.694,11.103 9.875,11.309C10.063,11.509 10.156,11.756 10.156,12.05C10.156,12.337 10.063,12.581 9.875,12.781C9.694,12.981 9.428,13.081 9.078,13.081C8.728,13.081 8.459,12.981 8.272,12.781C8.091,12.581 8,12.337 8,12.05Z" + android:fillColor="#151B2C"/> +</vector> diff --git a/app/src/test/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryTest.kt index eb12605bc..4a2df773a 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryTest.kt @@ -1099,7 +1099,7 @@ class AuthRepositoryTest { @Suppress("MaxLineLength") @Test - fun `switchAccount when the given userId is the same as the current activeUserId should do nothing`() { + fun `switchAccount when the given userId is the same as the current activeUserId should only clear any special circumstances`() { val originalUserId = USER_ID_1 val originalUserState = SINGLE_USER_STATE_1.toUserState( vaultState = VAULT_STATE, @@ -1110,6 +1110,7 @@ class AuthRepositoryTest { originalUserState, repository.userStateFlow.value, ) + repository.specialCircumstance = UserState.SpecialCircumstance.PendingAccountAddition assertEquals( SwitchAccountResult.NoChange, @@ -1120,6 +1121,7 @@ class AuthRepositoryTest { originalUserState, repository.userStateFlow.value, ) + assertNull(repository.specialCircumstance) verify(exactly = 0) { vaultRepository.lockVaultIfNecessary(originalUserId) } verify(exactly = 0) { vaultRepository.clearUnlockedData() } } @@ -1154,7 +1156,7 @@ class AuthRepositoryTest { @Suppress("MaxLineLength") @Test - fun `switchAccount when the userId is valid should update the current UserState, lock the vault of the previous active user, and clear the previously unlocked data`() { + fun `switchAccount when the userId is valid should update the current UserState, lock the vault of the previous active user, clear the previously unlocked data, and reset the special circumstance`() { val originalUserId = USER_ID_1 val updatedUserId = USER_ID_2 val originalUserState = MULTI_USER_STATE.toUserState( @@ -1166,6 +1168,7 @@ class AuthRepositoryTest { originalUserState, repository.userStateFlow.value, ) + repository.specialCircumstance = UserState.SpecialCircumstance.PendingAccountAddition assertEquals( SwitchAccountResult.AccountSwitched, @@ -1176,6 +1179,7 @@ class AuthRepositoryTest { originalUserState.copy(activeUserId = updatedUserId), repository.userStateFlow.value, ) + assertNull(repository.specialCircumstance) verify { vaultRepository.lockVaultIfNecessary(originalUserId) } verify { vaultRepository.clearUnlockedData() } } diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/landing/LandingScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/landing/LandingScreenTest.kt index 4778b624a..036bfd7e5 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/landing/LandingScreenTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/landing/LandingScreenTest.kt @@ -12,6 +12,7 @@ import androidx.compose.ui.test.filterToOne import androidx.compose.ui.test.hasAnyAncestor import androidx.compose.ui.test.isDialog import androidx.compose.ui.test.onAllNodesWithText +import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performScrollTo @@ -21,6 +22,7 @@ import com.x8bit.bitwarden.data.platform.repository.model.Environment import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest import com.x8bit.bitwarden.ui.platform.base.util.asText import com.x8bit.bitwarden.ui.platform.components.BasicDialogState +import com.x8bit.bitwarden.ui.platform.components.model.AccountSummary import io.mockk.every import io.mockk.mockk import io.mockk.verify @@ -63,6 +65,58 @@ class LandingScreenTest : BaseComposeTest() { } } + @Test + fun `account menu icon is present according to the state`() { + composeTestRule.onNodeWithContentDescription("Account").assertDoesNotExist() + + mutableStateFlow.update { + it.copy(accountSummaries = listOf(ACTIVE_ACCOUNT_SUMMARY)) + } + + composeTestRule.onNodeWithContentDescription("Account").assertIsDisplayed() + } + + @Test + fun `account menu icon click should show the account switcher`() { + mutableStateFlow.update { + it.copy(accountSummaries = listOf(ACTIVE_ACCOUNT_SUMMARY)) + } + + composeTestRule.onNodeWithContentDescription("Account").performClick() + + composeTestRule.onNodeWithText("active@bitwarden.com").assertIsDisplayed() + } + + @Suppress("MaxLineLength") + @Test + fun `account click in the account switcher should send SwitchAccountClick and close switcher`() { + // Show the account switcher + mutableStateFlow.update { + it.copy(accountSummaries = listOf(ACTIVE_ACCOUNT_SUMMARY)) + } + composeTestRule.onNodeWithContentDescription("Account").performClick() + composeTestRule.onNodeWithText("active@bitwarden.com").assertIsDisplayed() + + composeTestRule.onNodeWithText("active@bitwarden.com").performClick() + + verify { + viewModel.trySendAction(LandingAction.SwitchAccountClick(ACTIVE_ACCOUNT_SUMMARY)) + } + composeTestRule.onNodeWithText("active@bitwarden.com").assertDoesNotExist() + } + + @Test + fun `add account button in the account switcher does not exist`() { + // Show the account switcher + mutableStateFlow.update { + it.copy(accountSummaries = listOf(ACTIVE_ACCOUNT_SUMMARY)) + } + composeTestRule.onNodeWithContentDescription("Account").performClick() + composeTestRule.onNodeWithText("active@bitwarden.com").assertIsDisplayed() + + composeTestRule.onNodeWithText("Add account").assertDoesNotExist() + } + @Test fun `continue button should be enabled or disabled according to the state`() { composeTestRule.onNodeWithText("Continue").assertIsEnabled() @@ -224,10 +278,19 @@ class LandingScreenTest : BaseComposeTest() { } } +private val ACTIVE_ACCOUNT_SUMMARY = AccountSummary( + userId = "activeUserId", + name = "Active User", + email = "active@bitwarden.com", + avatarColorHex = "#aa00aa", + status = AccountSummary.Status.ACTIVE, +) + private val DEFAULT_STATE = LandingState( emailInput = "", isContinueButtonEnabled = true, isRememberMeEnabled = false, selectedEnvironmentType = Environment.Type.US, errorDialogState = BasicDialogState.Hidden, + accountSummaries = emptyList(), ) diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/landing/LandingViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/landing/LandingViewModelTest.kt index ac344c776..8455ff9bb 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/landing/LandingViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/landing/LandingViewModelTest.kt @@ -3,13 +3,16 @@ package com.x8bit.bitwarden.ui.auth.feature.landing import androidx.lifecycle.SavedStateHandle import app.cash.turbine.test import com.x8bit.bitwarden.R +import com.x8bit.bitwarden.data.auth.repository.model.UserState import com.x8bit.bitwarden.data.platform.repository.model.Environment import com.x8bit.bitwarden.data.platform.repository.util.FakeEnvironmentRepository import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest import com.x8bit.bitwarden.ui.platform.base.util.asText import com.x8bit.bitwarden.ui.platform.components.BasicDialogState +import com.x8bit.bitwarden.ui.vault.feature.vault.util.toAccountSummaries 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 @@ -42,6 +45,30 @@ class LandingViewModelTest : BaseViewModelTest() { } } + @Test + fun `initial state should set the account summaries based on the UserState`() { + val userState = UserState( + activeUserId = "activeUserId", + accounts = listOf( + UserState.Account( + userId = "activeUserId", + name = "name", + email = "email", + avatarColorHex = "avatarColorHex", + isPremium = true, + isVaultUnlocked = true, + ), + ), + ) + val viewModel = createViewModel(userState = userState) + assertEquals( + DEFAULT_STATE.copy( + accountSummaries = userState.toAccountSummaries(), + ), + viewModel.stateFlow.value, + ) + } + @Test fun `initial state should pull from saved state handle when present`() = runTest { val expectedState = DEFAULT_STATE.copy( @@ -180,10 +207,12 @@ class LandingViewModelTest : BaseViewModelTest() { private fun createViewModel( rememberedEmail: String? = null, + userState: UserState? = null, savedStateHandle: SavedStateHandle = SavedStateHandle(), ): LandingViewModel = LandingViewModel( authRepository = mockk(relaxed = true) { every { rememberedEmailAddress } returns rememberedEmail + every { userStateFlow } returns MutableStateFlow(userState) }, environmentRepository = fakeEnvironmentRepository, savedStateHandle = savedStateHandle, @@ -198,6 +227,7 @@ class LandingViewModelTest : BaseViewModelTest() { isRememberMeEnabled = false, selectedEnvironmentType = Environment.Type.US, errorDialogState = BasicDialogState.Hidden, + accountSummaries = emptyList(), ) } }