Implement initialization and logic for MigrateToMyItemsViewModel

Update the `MigrateToMyItemsViewModel` to replace placeholder data with actual repository logic, state persistence, and dialog management.

The view model now initializes by fetching the active organization details related to the personal ownership policy. It handles the "Continue" action by displaying a loading state (simulated for now) and processing the result to either navigate to the vault or show an error. Additionally, the help link now points to the correct documentation URL.

- Inject `AuthRepository`, `PolicyManager`, and `SavedStateHandle` into `MigrateToMyItemsViewModel`.
- Initialize `MigrateToMyItemsState` by resolving the `organizationId` via `PolicyManager` and retrieving the corresponding organization name from `AuthRepository` user state.
- Implement state persistence using `SavedStateHandle`.
- Refactor `MigrateToMyItemsState` to include a nested `ViewState` and a `DialogState` sealed class (supporting `Loading` and `Error` states).
- Update `handleContinueClicked` to set the dialog state to `Loading` and initiate a coroutine that currently simulates migration delay.
- Add `Internal` action handling to process migration results:
    - On success: Clears the dialog and triggers navigation to the vault.
    - On failure: Updates the dialog state to show an error message.
- Update `handleHelpLinkClicked` to launch the specific "transfer-ownership" help URI.
- Add new string resources `migrating_items_to_x` and `failed_to_migrate_items_to_x` for the new dialog states.
This commit is contained in:
Patrick Honkonen 2025-12-09 14:23:26 -05:00
parent 7fa3f50f51
commit e059c3a499
No known key found for this signature in database
GPG Key ID: 27C65CF8B03CC9FB
6 changed files with 499 additions and 35 deletions

View File

@ -13,7 +13,6 @@ import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
@ -30,12 +29,15 @@ import com.bitwarden.ui.platform.base.util.standardHorizontalMargin
import com.bitwarden.ui.platform.components.button.BitwardenFilledButton
import com.bitwarden.ui.platform.components.button.BitwardenOutlinedButton
import com.bitwarden.ui.platform.components.button.BitwardenTextButton
import com.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog
import com.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog
import com.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
import com.bitwarden.ui.platform.composition.LocalIntentManager
import com.bitwarden.ui.platform.manager.IntentManager
import com.bitwarden.ui.platform.resource.BitwardenDrawable
import com.bitwarden.ui.platform.resource.BitwardenString
import com.bitwarden.ui.platform.theme.BitwardenTheme
import com.x8bit.bitwarden.ui.vault.feature.migratetomyitems.handler.rememberMigrateToMyItemsHandler
/**
* Top level screen component for the MigrateToMyItems screen.
@ -48,6 +50,7 @@ fun MigrateToMyItemsScreen(
intentManager: IntentManager = LocalIntentManager.current,
) {
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
val handlers = rememberMigrateToMyItemsHandler(viewModel)
EventsEffect(viewModel = viewModel) { event ->
when (event) {
@ -57,22 +60,17 @@ fun MigrateToMyItemsScreen(
}
}
val onContinueClick = remember(viewModel) {
{ viewModel.trySendAction(MigrateToMyItemsAction.ContinueClicked) }
}
val onDeclineClick = remember(viewModel) {
{ viewModel.trySendAction(MigrateToMyItemsAction.DeclineAndLeaveClicked) }
}
val onHelpClick = remember(viewModel) {
{ viewModel.trySendAction(MigrateToMyItemsAction.HelpLinkClicked) }
}
MigrateToMyItemsDialogs(
dialog = state.dialog,
onDismissRequest = handlers.onDismissDialog,
)
BitwardenScaffold {
MigrateToMyItemsContent(
organizationName = state.organizationName,
onContinueClick = onContinueClick,
onDeclineClick = onDeclineClick,
onHelpClick = onHelpClick,
viewState = state.viewState,
onContinueClick = handlers.onContinueClick,
onDeclineClick = handlers.onDeclineClick,
onHelpClick = handlers.onHelpClick,
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState()),
@ -80,9 +78,31 @@ fun MigrateToMyItemsScreen(
}
}
@Composable
private fun MigrateToMyItemsDialogs(
dialog: MigrateToMyItemsState.DialogState?,
onDismissRequest: () -> Unit,
) {
when (dialog) {
is MigrateToMyItemsState.DialogState.Error -> {
BitwardenBasicDialog(
title = dialog.title(),
message = dialog.message(),
onDismissRequest = onDismissRequest,
)
}
is MigrateToMyItemsState.DialogState.Loading -> {
BitwardenLoadingDialog(text = dialog.message())
}
null -> Unit
}
}
@Composable
private fun MigrateToMyItemsContent(
organizationName: String,
viewState: MigrateToMyItemsState.ViewState,
onContinueClick: () -> Unit,
onDeclineClick: () -> Unit,
onHelpClick: () -> Unit,
@ -103,7 +123,7 @@ private fun MigrateToMyItemsContent(
.fillMaxWidth(),
)
Spacer(modifier = Modifier.height(24.dp))
MigrateToMyItemsTextContent(organizationName = organizationName)
MigrateToMyItemsTextContent(organizationName = viewState.organizationName)
Spacer(modifier = Modifier.height(24.dp))
MigrateToMyItemsActions(
onContinueClick = onContinueClick,
@ -188,7 +208,7 @@ private fun MigrateToMyItemsScreen_preview() {
BitwardenTheme {
BitwardenScaffold {
MigrateToMyItemsContent(
organizationName = "Test Organization",
viewState = MigrateToMyItemsState.ViewState(organizationName = "Bitwarden"),
onContinueClick = {},
onDeclineClick = {},
onHelpClick = {},

View File

@ -1,31 +1,93 @@
package com.x8bit.bitwarden.ui.vault.feature.migratetomyitems
import android.os.Parcelable
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import com.bitwarden.ui.platform.base.BaseViewModel
import com.bitwarden.ui.platform.resource.BitwardenString
import com.bitwarden.ui.util.Text
import com.bitwarden.ui.util.asText
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize
import javax.inject.Inject
private const val KEY_STATE = "state"
/**
* View model for the [MigrateToMyItemsScreen].
*/
@HiltViewModel
class MigrateToMyItemsViewModel @Inject constructor() :
BaseViewModel<MigrateToMyItemsState, MigrateToMyItemsEvent, MigrateToMyItemsAction>(
initialState = MigrateToMyItemsState(
// TODO: Get from repository or manager (PM-28468).
organizationName = "TODO",
),
) {
class MigrateToMyItemsViewModel @Inject constructor(
private val authRepository: AuthRepository,
private val policyManager: PolicyManager,
savedStateHandle: SavedStateHandle,
) : BaseViewModel<MigrateToMyItemsState, MigrateToMyItemsEvent, MigrateToMyItemsAction>(
initialState = savedStateHandle[KEY_STATE] ?: run {
val organizationId = requireNotNull(
policyManager.getPersonalOwnershipPolicyOrganizationId(),
)
val organization = requireNotNull(
authRepository
.userStateFlow
.value
?.activeAccount
?.organizations
?.firstOrNull { it.id == organizationId },
)
MigrateToMyItemsState(
organizationId = organizationId,
viewState = MigrateToMyItemsState.ViewState(
organizationName = organization.name.orEmpty(),
),
dialog = null,
)
},
) {
init {
stateFlow
.onEach { savedStateHandle[KEY_STATE] = it }
.launchIn(viewModelScope)
}
override fun handleAction(action: MigrateToMyItemsAction) {
when (action) {
MigrateToMyItemsAction.ContinueClicked -> handleContinueClicked()
MigrateToMyItemsAction.DeclineAndLeaveClicked -> handleDeclineAndLeaveClicked()
MigrateToMyItemsAction.HelpLinkClicked -> handleHelpLinkClicked()
MigrateToMyItemsAction.DismissDialogClicked -> handleDismissDialogClicked()
is MigrateToMyItemsAction.Internal -> handleInternalAction(action)
}
}
private fun handleContinueClicked() {
sendEvent(MigrateToMyItemsEvent.NavigateToVault)
mutableStateFlow.update {
it.copy(
dialog = MigrateToMyItemsState.DialogState.Loading(
message = BitwardenString.migrating_items_to_x.asText(
it.viewState.organizationName,
),
),
)
}
viewModelScope.launch {
// TODO: Replace `delay` with actual migration (PM-28444).
delay(timeMillis = 100L)
trySendAction(
MigrateToMyItemsAction.Internal.MigrateToMyItemsResultReceived(
success = true,
),
)
}
}
private fun handleDeclineAndLeaveClicked() {
@ -33,17 +95,87 @@ class MigrateToMyItemsViewModel @Inject constructor() :
}
private fun handleHelpLinkClicked() {
// TODO: Update URL when available.
sendEvent(MigrateToMyItemsEvent.LaunchUri("TODO_HELP_URL"))
sendEvent(
MigrateToMyItemsEvent.LaunchUri(
uri = "https://bitwarden.com/help/transfer-ownership/",
),
)
}
private fun handleDismissDialogClicked() {
clearDialog()
}
private fun handleInternalAction(action: MigrateToMyItemsAction.Internal) {
when (action) {
is MigrateToMyItemsAction.Internal.MigrateToMyItemsResultReceived -> {
handleMigrateToMyItemsResultReceived(action)
}
}
}
private fun handleMigrateToMyItemsResultReceived(
action: MigrateToMyItemsAction.Internal.MigrateToMyItemsResultReceived,
) {
if (action.success) {
clearDialog()
sendEvent(MigrateToMyItemsEvent.NavigateToVault)
} else {
mutableStateFlow.update {
it.copy(
dialog = MigrateToMyItemsState.DialogState.Error(
title = BitwardenString.an_error_has_occurred.asText(),
message = BitwardenString.failed_to_migrate_items_to_x.asText(),
),
)
}
}
}
private fun clearDialog() {
mutableStateFlow.update { it.copy(dialog = null) }
}
}
/**
* Models the state for the [MigrateToMyItemsScreen].
*/
@Parcelize
data class MigrateToMyItemsState(
val organizationName: String,
)
val viewState: ViewState,
val dialog: DialogState?,
// Internal view model properties
private val organizationId: String,
) : Parcelable {
/**
* Models the view state for the [MigrateToMyItemsScreen].
*/
@Parcelize
data class ViewState(
val organizationName: String,
) : Parcelable
/**
* Models the dialog state for the [MigrateToMyItemsScreen].
*/
sealed class DialogState : Parcelable {
/**
* Displays a loading dialog.
*/
@Parcelize
data class Loading(val message: Text) : DialogState()
/**
* Displays an error dialog.
*/
@Parcelize
data class Error(
val title: Text,
val message: Text,
) : DialogState()
}
}
/**
* Models the events that can be sent from the [MigrateToMyItemsViewModel].
@ -83,4 +215,23 @@ sealed class MigrateToMyItemsAction {
* User clicked the "Why am I seeing this?" help link.
*/
data object HelpLinkClicked : MigrateToMyItemsAction()
/**
* User dismissed the dialog.
*/
data object DismissDialogClicked : MigrateToMyItemsAction()
/**
* Models internal actions that the [MigrateToMyItemsViewModel] itself may send.
*/
sealed class Internal : MigrateToMyItemsAction() {
/**
* The result of the migration has been received.
*/
data class MigrateToMyItemsResultReceived(
// TODO: Replace `success` with actual migration result (PM-28444).
val success: Boolean,
) : Internal()
}
}

View File

@ -0,0 +1,46 @@
package com.x8bit.bitwarden.ui.vault.feature.migratetomyitems.handler
import androidx.compose.runtime.Composable
import com.x8bit.bitwarden.ui.vault.feature.migratetomyitems.MigrateToMyItemsAction
import com.x8bit.bitwarden.ui.vault.feature.migratetomyitems.MigrateToMyItemsScreen
import com.x8bit.bitwarden.ui.vault.feature.migratetomyitems.MigrateToMyItemsViewModel
/**
* Action handlers for the [MigrateToMyItemsScreen].
*/
class MigrateToMyItemsHandler(
val onContinueClick: () -> Unit,
val onDeclineClick: () -> Unit,
val onHelpClick: () -> Unit,
val onDismissDialog: () -> Unit,
) {
@Suppress("UndocumentedPublicClass")
companion object {
/**
* Creates an instance of [MigrateToMyItemsHandler] using the provided
* [MigrateToMyItemsViewModel].
*/
fun create(viewModel: MigrateToMyItemsViewModel) = MigrateToMyItemsHandler(
onContinueClick = {
viewModel.trySendAction(MigrateToMyItemsAction.ContinueClicked)
},
onDeclineClick = {
viewModel.trySendAction(MigrateToMyItemsAction.DeclineAndLeaveClicked)
},
onHelpClick = {
viewModel.trySendAction(MigrateToMyItemsAction.HelpLinkClicked)
},
onDismissDialog = {
viewModel.trySendAction(MigrateToMyItemsAction.DismissDialogClicked)
},
)
}
}
/**
* Helper function to remember a [MigrateToMyItemsHandler] instance in a [Composable] scope.
*/
@Composable
fun rememberMigrateToMyItemsHandler(viewModel: MigrateToMyItemsViewModel): MigrateToMyItemsHandler =
MigrateToMyItemsHandler.create(viewModel)

View File

@ -3,10 +3,16 @@ package com.x8bit.bitwarden.ui.vault.feature.migratetomyitems
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.core.net.toUri
import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow
import com.bitwarden.ui.platform.manager.IntentManager
import com.bitwarden.ui.platform.resource.BitwardenString
import com.bitwarden.ui.util.asText
import com.x8bit.bitwarden.ui.platform.base.BitwardenComposeTest
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import io.mockk.runs
import io.mockk.verify
import kotlinx.coroutines.flow.MutableStateFlow
import org.junit.Before
@ -17,11 +23,19 @@ class MigrateToMyItemsScreenTest : BitwardenComposeTest() {
private var onNavigateToVaultCalled = false
private var onNavigateToLeaveOrganizationCalled = false
private val intentManager: IntentManager = mockk {
every { launchUri(any()) } just runs
}
private val mutableEventFlow = bufferedMutableSharedFlow<MigrateToMyItemsEvent>()
private val mutableStateFlow = MutableStateFlow(
MigrateToMyItemsState(
organizationName = "Test Organization",
viewState = MigrateToMyItemsState.ViewState(
organizationName = "Test Organization",
),
dialog = null,
organizationId = "test-org-id",
),
)
@ -32,7 +46,7 @@ class MigrateToMyItemsScreenTest : BitwardenComposeTest() {
@Before
fun setup() {
setContent {
setContent(intentManager = intentManager) {
MigrateToMyItemsScreen(
viewModel = viewModel,
onNavigateToVault = { onNavigateToVaultCalled = true },
@ -98,4 +112,73 @@ class MigrateToMyItemsScreenTest : BitwardenComposeTest() {
mutableEventFlow.tryEmit(MigrateToMyItemsEvent.NavigateToLeaveOrganization)
assertTrue(onNavigateToLeaveOrganizationCalled)
}
@Test
fun `LaunchUri event should launch URI via intent manager`() {
val testUri = "https://bitwarden.com/help/transfer-ownership/"
mutableEventFlow.tryEmit(MigrateToMyItemsEvent.LaunchUri(testUri))
verify {
intentManager.launchUri(testUri.toUri())
}
}
@Test
fun `Loading dialog should display when dialog state is Loading`() {
mutableStateFlow.value = MigrateToMyItemsState(
viewState = MigrateToMyItemsState.ViewState(
organizationName = "Test Organization",
),
dialog = MigrateToMyItemsState.DialogState.Loading(
message = "Migrating items to Test Organization...".asText(),
),
organizationId = "test-org-id",
)
composeTestRule
.onNodeWithText("Migrating items to Test Organization...")
.assertIsDisplayed()
}
@Test
fun `Error dialog should display when dialog state is Error`() {
mutableStateFlow.value = MigrateToMyItemsState(
viewState = MigrateToMyItemsState.ViewState(
organizationName = "Test Organization",
),
dialog = MigrateToMyItemsState.DialogState.Error(
title = "An error has occurred".asText(),
message = "Failed to migrate items".asText(),
),
organizationId = "test-org-id",
)
composeTestRule
.onNodeWithText("An error has occurred")
.assertIsDisplayed()
composeTestRule
.onNodeWithText("Failed to migrate items")
.assertIsDisplayed()
}
@Test
fun `Error dialog dismiss should send DismissDialogClicked action`() {
mutableStateFlow.value = MigrateToMyItemsState(
viewState = MigrateToMyItemsState.ViewState(
organizationName = "Test Organization",
),
dialog = MigrateToMyItemsState.DialogState.Error(
title = BitwardenString.an_error_has_occurred.asText(),
message = "Failed to migrate items".asText(),
),
organizationId = "test-org-id",
)
composeTestRule
.onNodeWithText("Okay")
.performClick()
verify {
viewModel.trySendAction(MigrateToMyItemsAction.DismissDialogClicked)
}
}
}

View File

@ -1,15 +1,63 @@
package com.x8bit.bitwarden.ui.vault.feature.migratetomyitems
import androidx.lifecycle.SavedStateHandle
import app.cash.turbine.test
import com.bitwarden.data.repository.model.Environment
import com.bitwarden.network.model.OrganizationType
import com.bitwarden.ui.platform.base.BaseViewModelTest
import com.bitwarden.ui.platform.resource.BitwardenString
import com.bitwarden.ui.util.asText
import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.model.Organization
import com.x8bit.bitwarden.data.auth.repository.model.UserState
import com.x8bit.bitwarden.data.auth.repository.model.VaultUnlockType
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
import com.x8bit.bitwarden.data.platform.manager.model.FirstTimeState
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.Assertions.assertNull
import org.junit.jupiter.api.Test
class MigrateToMyItemsViewModelTest : BaseViewModelTest() {
private val authRepository = mockk<AuthRepository> {
every { userStateFlow } returns MutableStateFlow(DEFAULT_USER_STATE)
}
private val policyManager = mockk<PolicyManager> {
every { getPersonalOwnershipPolicyOrganizationId() } returns ORGANIZATION_ID
}
@Test
fun `ContinueClicked sends NavigateToVault event`() = runTest {
fun `initial state should be set from organization data`() {
val viewModel = createViewModel()
assertEquals(ORGANIZATION_NAME, viewModel.stateFlow.value.viewState.organizationName)
assertNull(viewModel.stateFlow.value.dialog)
}
@Test
fun `ContinueClicked should show loading dialog and trigger migration`() = runTest {
val viewModel = createViewModel()
viewModel.stateFlow.test {
assertEquals(null, awaitItem().dialog)
viewModel.trySendAction(MigrateToMyItemsAction.ContinueClicked)
val loadingState = awaitItem()
assert(loadingState.dialog is MigrateToMyItemsState.DialogState.Loading)
assertEquals(
BitwardenString.migrating_items_to_x.asText(ORGANIZATION_NAME),
(loadingState.dialog as MigrateToMyItemsState.DialogState.Loading).message,
)
}
}
@Test
fun `ContinueClicked should navigate to vault on success`() = runTest {
val viewModel = createViewModel()
viewModel.eventFlow.test {
viewModel.trySendAction(MigrateToMyItemsAction.ContinueClicked)
@ -17,6 +65,47 @@ class MigrateToMyItemsViewModelTest : BaseViewModelTest() {
}
}
@Test
fun `MigrateToMyItemsResultReceived with success should clear dialog and navigate to vault`() =
runTest {
val viewModel = createViewModel()
// First show the loading dialog
viewModel.trySendAction(MigrateToMyItemsAction.ContinueClicked)
viewModel.eventFlow.test {
viewModel.trySendAction(
MigrateToMyItemsAction.Internal.MigrateToMyItemsResultReceived(
success = true,
),
)
assertEquals(MigrateToMyItemsEvent.NavigateToVault, awaitItem())
}
assertNull(viewModel.stateFlow.value.dialog)
}
@Test
fun `MigrateToMyItemsResultReceived with failure should show error dialog`() = runTest {
val viewModel = createViewModel()
viewModel.stateFlow.test {
awaitItem() // Initial state
viewModel.trySendAction(
MigrateToMyItemsAction.Internal.MigrateToMyItemsResultReceived(
success = false,
),
)
val errorState = awaitItem()
assert(errorState.dialog is MigrateToMyItemsState.DialogState.Error)
val errorDialog = errorState.dialog as MigrateToMyItemsState.DialogState.Error
assertEquals(BitwardenString.an_error_has_occurred.asText(), errorDialog.title)
assertEquals(BitwardenString.failed_to_migrate_items_to_x.asText(), errorDialog.message)
}
}
@Test
fun `DeclineAndLeaveClicked sends NavigateToLeaveOrganization event`() = runTest {
val viewModel = createViewModel()
@ -33,10 +122,83 @@ class MigrateToMyItemsViewModelTest : BaseViewModelTest() {
viewModel.trySendAction(MigrateToMyItemsAction.HelpLinkClicked)
val event = awaitItem()
assert(event is MigrateToMyItemsEvent.LaunchUri)
assertEquals("TODO_HELP_URL", (event as MigrateToMyItemsEvent.LaunchUri).uri)
assertEquals(
"https://bitwarden.com/help/transfer-ownership/",
(event as MigrateToMyItemsEvent.LaunchUri).uri,
)
}
}
private fun createViewModel(): MigrateToMyItemsViewModel =
MigrateToMyItemsViewModel()
@Test
fun `DismissDialogClicked should clear dialog`() = runTest {
val viewModel = createViewModel()
viewModel.stateFlow.test {
awaitItem() // Initial state
// First show an error dialog
viewModel.trySendAction(
MigrateToMyItemsAction.Internal.MigrateToMyItemsResultReceived(
success = false,
),
)
val errorState = awaitItem()
assert(errorState.dialog is MigrateToMyItemsState.DialogState.Error)
// Dismiss the dialog
viewModel.trySendAction(MigrateToMyItemsAction.DismissDialogClicked)
val clearedState = awaitItem()
assertNull(clearedState.dialog)
}
}
private fun createViewModel(
savedStateHandle: SavedStateHandle = SavedStateHandle(),
): MigrateToMyItemsViewModel =
MigrateToMyItemsViewModel(
authRepository = authRepository,
policyManager = policyManager,
savedStateHandle = savedStateHandle,
)
}
private const val ORGANIZATION_ID = "test-organization-id"
private const val ORGANIZATION_NAME = "Test Organization"
private val DEFAULT_ORGANIZATION = Organization(
id = ORGANIZATION_ID,
name = ORGANIZATION_NAME,
shouldManageResetPassword = false,
shouldUseKeyConnector = false,
role = OrganizationType.USER,
keyConnectorUrl = null,
userIsClaimedByOrganization = false,
limitItemDeletion = false,
)
private val DEFAULT_USER_STATE = UserState(
activeUserId = "test-user-id",
accounts = listOf(
UserState.Account(
userId = "test-user-id",
name = "Test User",
email = "test@example.com",
avatarColorHex = "#ff0000",
environment = Environment.Us,
isPremium = false,
isLoggedIn = true,
isVaultUnlocked = true,
needsPasswordReset = false,
needsMasterPassword = false,
hasMasterPassword = true,
trustedDevice = null,
organizations = listOf(DEFAULT_ORGANIZATION),
isBiometricsEnabled = false,
vaultUnlockType = VaultUnlockType.MASTER_PASSWORD,
isUsingKeyConnector = false,
onboardingStatus = OnboardingStatus.COMPLETE,
firstTimeState = FirstTimeState(),
isExportable = true,
),
),
)

View File

@ -1165,4 +1165,6 @@ Do you want to switch to this account?</string>
<string name="transfer_items_description">%1$s is requiring all items to be owned by the organization for security and compliance. Click accept to transfer ownership of your items.</string>
<string name="decline_and_leave">Decline and leave</string>
<string name="why_am_i_seeing_this">Why am I seeing this?</string>
<string name="migrating_items_to_x">Migrating items to %s</string>
<string name="failed_to_migrate_items_to_x">Failed to migrate items to %s</string>
</resources>