mirror of
https://github.com/bitwarden/android.git
synced 2025-02-19 21:39:57 +03:00
Adding the UI and ViewModel (#700)
This commit is contained in:
parent
cd1d326d45
commit
ffba00bf83
7 changed files with 1705 additions and 51 deletions
|
@ -0,0 +1,179 @@
|
||||||
|
package com.x8bit.bitwarden.ui.vault.feature.verificationcode
|
||||||
|
|
||||||
|
import androidx.compose.animation.core.LinearOutSlowInEasing
|
||||||
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.defaultMinSize
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.material.ripple.rememberRipple
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
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.graphics.StrokeCap
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.x8bit.bitwarden.R
|
||||||
|
import com.x8bit.bitwarden.ui.platform.components.BitwardenIcon
|
||||||
|
import com.x8bit.bitwarden.ui.platform.components.model.IconData
|
||||||
|
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The verification code item displayed to the user.
|
||||||
|
*
|
||||||
|
* @param authCode The code for the item.
|
||||||
|
* @param label The label for the item.
|
||||||
|
* @param periodSeconds The times span where the code is valid.
|
||||||
|
* @param timeLeftSeconds The seconds remaining until a new code is needed.
|
||||||
|
* @param startIcon The leading icon for the item.
|
||||||
|
* @param onCopyClick The lambda function to be invoked when the copy button is clicked.
|
||||||
|
* @param onItemClick The lambda function to be invoked when the item is clicked.
|
||||||
|
* @param modifier The modifier for the item.
|
||||||
|
* @param supportingLabel The supporting label for the item.
|
||||||
|
*/
|
||||||
|
@Suppress("LongMethod", "MagicNumber")
|
||||||
|
@Composable
|
||||||
|
fun VaultVerificationCodeItem(
|
||||||
|
authCode: String,
|
||||||
|
label: String,
|
||||||
|
periodSeconds: Int,
|
||||||
|
timeLeftSeconds: Int,
|
||||||
|
startIcon: IconData,
|
||||||
|
onCopyClick: () -> Unit,
|
||||||
|
onItemClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
supportingLabel: String? = null,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.clickable(
|
||||||
|
interactionSource = remember { MutableInteractionSource() },
|
||||||
|
indication = rememberRipple(color = MaterialTheme.colorScheme.primary),
|
||||||
|
onClick = onItemClick,
|
||||||
|
)
|
||||||
|
.defaultMinSize(minHeight = 72.dp)
|
||||||
|
.padding(vertical = 8.dp)
|
||||||
|
.then(modifier),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
|
) {
|
||||||
|
BitwardenIcon(
|
||||||
|
iconData = startIcon,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.onSurface,
|
||||||
|
modifier = Modifier.size(24.dp),
|
||||||
|
)
|
||||||
|
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.Start,
|
||||||
|
verticalArrangement = Arrangement.SpaceEvenly,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = label,
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
|
||||||
|
supportingLabel?.let {
|
||||||
|
Text(
|
||||||
|
text = it,
|
||||||
|
maxLines = 1,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CircularIndicator(
|
||||||
|
timeLeftSeconds = timeLeftSeconds,
|
||||||
|
periodSeconds = periodSeconds,
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = authCode.chunked(3).joinToString(" "),
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
|
||||||
|
IconButton(
|
||||||
|
onClick = onCopyClick,
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(id = R.drawable.ic_copy),
|
||||||
|
contentDescription = stringResource(id = R.string.copy),
|
||||||
|
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
modifier = Modifier.size(24.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun CircularIndicator(
|
||||||
|
timeLeftSeconds: Int,
|
||||||
|
periodSeconds: Int,
|
||||||
|
) {
|
||||||
|
val progressAnimate by animateFloatAsState(
|
||||||
|
targetValue = timeLeftSeconds.toFloat() / periodSeconds,
|
||||||
|
animationSpec = tween(
|
||||||
|
durationMillis = periodSeconds,
|
||||||
|
delayMillis = 0,
|
||||||
|
easing = LinearOutSlowInEasing,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
Box(contentAlignment = Alignment.Center) {
|
||||||
|
|
||||||
|
CircularProgressIndicator(
|
||||||
|
progress = { progressAnimate },
|
||||||
|
modifier = Modifier.size(size = 50.dp),
|
||||||
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
strokeWidth = 3.dp,
|
||||||
|
strokeCap = StrokeCap.Round,
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = timeLeftSeconds.toString(),
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("MagicNumber")
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
private fun VerificationCodeItem_preview() {
|
||||||
|
BitwardenTheme {
|
||||||
|
VaultVerificationCodeItem(
|
||||||
|
startIcon = IconData.Local(R.drawable.ic_login_item),
|
||||||
|
label = "Sample Label",
|
||||||
|
supportingLabel = "Supporting Label",
|
||||||
|
authCode = "1234567890".chunked(3).joinToString(" "),
|
||||||
|
timeLeftSeconds = 15,
|
||||||
|
periodSeconds = 30,
|
||||||
|
onCopyClick = {},
|
||||||
|
onItemClick = {},
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,49 +1,80 @@
|
||||||
package com.x8bit.bitwarden.ui.vault.feature.verificationcode
|
package com.x8bit.bitwarden.ui.vault.feature.verificationcode
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
|
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
|
||||||
import androidx.compose.material3.rememberTopAppBarState
|
import androidx.compose.material3.rememberTopAppBarState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|
||||||
import com.x8bit.bitwarden.R
|
import com.x8bit.bitwarden.R
|
||||||
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
|
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
|
||||||
|
import com.x8bit.bitwarden.ui.platform.base.util.showNotYetImplementedToast
|
||||||
|
import com.x8bit.bitwarden.ui.platform.components.BitwardenErrorContent
|
||||||
|
import com.x8bit.bitwarden.ui.platform.components.BitwardenListHeaderTextWithSupportLabel
|
||||||
|
import com.x8bit.bitwarden.ui.platform.components.BitwardenLoadingContent
|
||||||
|
import com.x8bit.bitwarden.ui.platform.components.BitwardenLoadingDialog
|
||||||
|
import com.x8bit.bitwarden.ui.platform.components.BitwardenOverflowActionItem
|
||||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenScaffold
|
import com.x8bit.bitwarden.ui.platform.components.BitwardenScaffold
|
||||||
|
import com.x8bit.bitwarden.ui.platform.components.BitwardenSearchActionItem
|
||||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenTopAppBar
|
import com.x8bit.bitwarden.ui.platform.components.BitwardenTopAppBar
|
||||||
|
import com.x8bit.bitwarden.ui.platform.components.LoadingDialogState
|
||||||
|
import com.x8bit.bitwarden.ui.platform.components.OverflowMenuItemData
|
||||||
|
import com.x8bit.bitwarden.ui.vault.feature.verificationcode.handlers.VerificationCodeHandlers
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Displays the verification codes to the user.
|
* Displays the verification codes to the user.
|
||||||
*/
|
*/
|
||||||
|
@Suppress("LongMethod")
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun VerificationCodeScreen(
|
fun VerificationCodeScreen(
|
||||||
|
viewModel: VerificationCodeViewModel = hiltViewModel(),
|
||||||
onNavigateBack: () -> Unit,
|
onNavigateBack: () -> Unit,
|
||||||
onNavigateToVaultItemScreen: (String) -> Unit,
|
onNavigateToVaultItemScreen: (String) -> Unit,
|
||||||
viewModel: VerificationCodeViewModel = hiltViewModel(),
|
|
||||||
) {
|
) {
|
||||||
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
|
val state by viewModel.stateFlow.collectAsState()
|
||||||
|
val context = LocalContext.current
|
||||||
|
val verificationCodeHandler = remember(viewModel) {
|
||||||
|
VerificationCodeHandlers.create(viewModel)
|
||||||
|
}
|
||||||
|
|
||||||
|
val pullToRefreshState = rememberPullToRefreshState().takeIf { state.isPullToRefreshEnabled }
|
||||||
|
LaunchedEffect(key1 = pullToRefreshState?.isRefreshing) {
|
||||||
|
if (pullToRefreshState?.isRefreshing == true) {
|
||||||
|
viewModel.trySendAction(VerificationCodeAction.RefreshPull)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
EventsEffect(viewModel = viewModel) { event ->
|
EventsEffect(viewModel = viewModel) { event ->
|
||||||
when (event) {
|
when (event) {
|
||||||
is VerificationCodeEvent.NavigateBack -> onNavigateBack.invoke()
|
is VerificationCodeEvent.DismissPullToRefresh -> pullToRefreshState?.endRefresh()
|
||||||
|
is VerificationCodeEvent.NavigateBack -> onNavigateBack()
|
||||||
is VerificationCodeEvent.NavigateToVaultItem -> onNavigateToVaultItemScreen(event.id)
|
is VerificationCodeEvent.NavigateToVaultItem -> onNavigateToVaultItemScreen(event.id)
|
||||||
|
is VerificationCodeEvent.NavigateToVaultSearchScreen -> {
|
||||||
|
showNotYetImplementedToast(context = context)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
VerificationCodeDialogs(dialogState = state.dialogState)
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||||
BitwardenScaffold(
|
BitwardenScaffold(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
@ -51,25 +82,113 @@ fun VerificationCodeScreen(
|
||||||
.nestedScroll(scrollBehavior.nestedScrollConnection),
|
.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||||
topBar = {
|
topBar = {
|
||||||
BitwardenTopAppBar(
|
BitwardenTopAppBar(
|
||||||
title = stringResource(id = R.string.verification_codes),
|
title = stringResource(id = R.string.verification_code),
|
||||||
scrollBehavior = scrollBehavior,
|
scrollBehavior = scrollBehavior,
|
||||||
navigationIcon = painterResource(id = R.drawable.ic_back),
|
navigationIcon = painterResource(id = R.drawable.ic_back),
|
||||||
navigationIconContentDescription = stringResource(id = R.string.back),
|
navigationIconContentDescription = stringResource(id = R.string.back),
|
||||||
onNavigationIconClick = remember(viewModel) {
|
onNavigationIconClick = verificationCodeHandler.backClick,
|
||||||
{ viewModel.trySendAction(VerificationCodeAction.BackClick) }
|
actions = {
|
||||||
|
BitwardenSearchActionItem(
|
||||||
|
contentDescription = stringResource(id = R.string.search_vault),
|
||||||
|
onClick = verificationCodeHandler.searchIconClick,
|
||||||
|
)
|
||||||
|
BitwardenOverflowActionItem(
|
||||||
|
menuItemDataList = persistentListOf(
|
||||||
|
OverflowMenuItemData(
|
||||||
|
text = stringResource(id = R.string.sync),
|
||||||
|
onClick = verificationCodeHandler.syncClick,
|
||||||
|
),
|
||||||
|
OverflowMenuItemData(
|
||||||
|
text = stringResource(id = R.string.lock),
|
||||||
|
onClick = verificationCodeHandler.lockClick,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
) { innerPadding ->
|
pullToRefreshState = pullToRefreshState,
|
||||||
Column(
|
) { paddingValues ->
|
||||||
modifier = Modifier
|
val modifier = Modifier
|
||||||
.padding(innerPadding)
|
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.verticalScroll(rememberScrollState()),
|
.padding(paddingValues)
|
||||||
verticalArrangement = Arrangement.Center,
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
when (val viewState = state.viewState) {
|
||||||
) {
|
is VerificationCodeState.ViewState.Content -> {
|
||||||
Text(text = "Not yet implemented")
|
VerificationCodeContent(
|
||||||
|
items = viewState.verificationCodeDisplayItems,
|
||||||
|
onCopyClick = verificationCodeHandler.copyClick,
|
||||||
|
itemClick = verificationCodeHandler.itemClick,
|
||||||
|
modifier = modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is VerificationCodeState.ViewState.Error -> {
|
||||||
|
BitwardenErrorContent(
|
||||||
|
message = viewState.message.invoke(),
|
||||||
|
onTryAgainClick = verificationCodeHandler.refreshClick,
|
||||||
|
modifier = modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is VerificationCodeState.ViewState.Loading -> {
|
||||||
|
BitwardenLoadingContent(modifier = modifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
is VerificationCodeState.ViewState.NoItems -> Unit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun VerificationCodeDialogs(
|
||||||
|
dialogState: VerificationCodeState.DialogState?,
|
||||||
|
) {
|
||||||
|
when (dialogState) {
|
||||||
|
is VerificationCodeState.DialogState.Loading -> BitwardenLoadingDialog(
|
||||||
|
visibilityState = LoadingDialogState.Shown(dialogState.message),
|
||||||
|
)
|
||||||
|
|
||||||
|
null -> Unit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun VerificationCodeContent(
|
||||||
|
items: List<VerificationCodeDisplayItem>,
|
||||||
|
itemClick: (id: String) -> Unit,
|
||||||
|
onCopyClick: (text: String) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
LazyColumn(
|
||||||
|
modifier = modifier,
|
||||||
|
) {
|
||||||
|
item {
|
||||||
|
BitwardenListHeaderTextWithSupportLabel(
|
||||||
|
label = stringResource(id = R.string.items),
|
||||||
|
supportingLabel = items.size.toString(),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
items(items) {
|
||||||
|
VaultVerificationCodeItem(
|
||||||
|
startIcon = it.startIcon,
|
||||||
|
label = it.label,
|
||||||
|
supportingLabel = it.supportingLabel,
|
||||||
|
timeLeftSeconds = it.timeLeftSeconds,
|
||||||
|
periodSeconds = it.periodSeconds,
|
||||||
|
authCode = it.authCode,
|
||||||
|
onCopyClick = { onCopyClick(it.authCode) },
|
||||||
|
onItemClick = {
|
||||||
|
itemClick(it.id)
|
||||||
|
},
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,58 +1,299 @@
|
||||||
package com.x8bit.bitwarden.ui.vault.feature.verificationcode
|
package com.x8bit.bitwarden.ui.vault.feature.verificationcode
|
||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import androidx.lifecycle.SavedStateHandle
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.bitwarden.core.CipherType
|
||||||
|
import com.x8bit.bitwarden.R
|
||||||
|
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager
|
||||||
|
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
||||||
|
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||||
|
import com.x8bit.bitwarden.data.platform.repository.model.DataState
|
||||||
|
import com.x8bit.bitwarden.data.platform.repository.util.baseIconUrl
|
||||||
|
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||||
|
import com.x8bit.bitwarden.data.vault.repository.model.VaultData
|
||||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
|
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
|
||||||
|
import com.x8bit.bitwarden.ui.platform.base.util.Text
|
||||||
|
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||||
|
import com.x8bit.bitwarden.ui.platform.base.util.concat
|
||||||
|
import com.x8bit.bitwarden.ui.platform.components.model.IconData
|
||||||
|
import com.x8bit.bitwarden.ui.vault.feature.vault.model.VaultFilterType
|
||||||
|
import com.x8bit.bitwarden.ui.vault.feature.vault.util.toFilteredList
|
||||||
|
import com.x8bit.bitwarden.ui.vault.feature.verificationcode.util.toVerificationCodeViewState
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
private const val KEY_STATE = "state"
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles [VerificationCodeAction],
|
* Handles [VerificationCodeAction],
|
||||||
* and launches [VerificationCodeEvent] for the [VerificationCodeScreen].
|
* and launches [VerificationCodeEvent] for the [VerificationCodeScreen].
|
||||||
*/
|
*/
|
||||||
|
@Suppress("TooManyFunctions")
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class VerificationCodeViewModel @Inject constructor(
|
class VerificationCodeViewModel @Inject constructor(
|
||||||
savedStateHandle: SavedStateHandle,
|
private val clipboardManager: BitwardenClipboardManager,
|
||||||
|
private val environmentRepository: EnvironmentRepository,
|
||||||
|
private val settingsRepository: SettingsRepository,
|
||||||
|
private val vaultRepository: VaultRepository,
|
||||||
) : BaseViewModel<VerificationCodeState, VerificationCodeEvent, VerificationCodeAction>(
|
) : BaseViewModel<VerificationCodeState, VerificationCodeEvent, VerificationCodeAction>(
|
||||||
initialState = savedStateHandle[KEY_STATE]
|
initialState = run {
|
||||||
?: VerificationCodeState(
|
VerificationCodeState(
|
||||||
viewState = VerificationCodeState.ViewState.Empty,
|
baseIconUrl = environmentRepository.environment.environmentUrlData.baseIconUrl,
|
||||||
),
|
isIconLoadingDisabled = settingsRepository.isIconLoadingDisabled,
|
||||||
|
isPullToRefreshSettingEnabled = settingsRepository.getPullToRefreshEnabledFlow().value,
|
||||||
|
vaultFilterType = vaultRepository.vaultFilterType,
|
||||||
|
viewState = VerificationCodeState.ViewState.Loading,
|
||||||
|
dialogState = null,
|
||||||
|
)
|
||||||
|
},
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
init {
|
||||||
|
settingsRepository
|
||||||
|
.getPullToRefreshEnabledFlow()
|
||||||
|
.map { VerificationCodeAction.Internal.PullToRefreshEnableReceive(it) }
|
||||||
|
.onEach(::sendAction)
|
||||||
|
.launchIn(viewModelScope)
|
||||||
|
|
||||||
|
settingsRepository
|
||||||
|
.isIconLoadingDisabledFlow
|
||||||
|
.map { VerificationCodeAction.Internal.IconLoadingSettingReceive(it) }
|
||||||
|
.onEach(::sendAction)
|
||||||
|
.launchIn(viewModelScope)
|
||||||
|
|
||||||
|
vaultRepository
|
||||||
|
.vaultDataStateFlow
|
||||||
|
.onEach { sendAction(VerificationCodeAction.Internal.VaultDataReceive(vaultData = it)) }
|
||||||
|
.launchIn(viewModelScope)
|
||||||
|
}
|
||||||
|
|
||||||
override fun handleAction(action: VerificationCodeAction) {
|
override fun handleAction(action: VerificationCodeAction) {
|
||||||
when (action) {
|
when (action) {
|
||||||
is VerificationCodeAction.BackClick -> handleBackClick()
|
is VerificationCodeAction.BackClick -> handleBackClick()
|
||||||
|
is VerificationCodeAction.CopyClick -> handleCopyClick(action)
|
||||||
is VerificationCodeAction.ItemClick -> handleItemClick(action)
|
is VerificationCodeAction.ItemClick -> handleItemClick(action)
|
||||||
|
is VerificationCodeAction.LockClick -> handleLockClick()
|
||||||
|
is VerificationCodeAction.RefreshClick -> handleRefreshClick()
|
||||||
|
is VerificationCodeAction.RefreshPull -> handleRefreshPull()
|
||||||
|
is VerificationCodeAction.SearchIconClick -> handleSearchIconClick()
|
||||||
|
is VerificationCodeAction.SyncClick -> handleSyncClick()
|
||||||
|
is VerificationCodeAction.Internal -> handleInternalAction(action)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//region VerificationCode Handlers
|
||||||
private fun handleBackClick() {
|
private fun handleBackClick() {
|
||||||
sendEvent(
|
sendEvent(
|
||||||
event = VerificationCodeEvent.NavigateBack,
|
event = VerificationCodeEvent.NavigateBack,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun handleCopyClick(action: VerificationCodeAction.CopyClick) {
|
||||||
|
clipboardManager.setText(text = action.text)
|
||||||
|
}
|
||||||
|
|
||||||
private fun handleItemClick(action: VerificationCodeAction.ItemClick) {
|
private fun handleItemClick(action: VerificationCodeAction.ItemClick) {
|
||||||
sendEvent(
|
sendEvent(
|
||||||
VerificationCodeEvent.NavigateToVaultItem(action.id),
|
VerificationCodeEvent.NavigateToVaultItem(action.id),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun handleLockClick() {
|
||||||
|
vaultRepository.lockVaultForCurrentUser()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleRefreshClick() {
|
||||||
|
vaultRepository.sync()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleRefreshPull() {
|
||||||
|
// The Pull-To-Refresh composable is already in the refreshing state.
|
||||||
|
// We will reset that state when sendDataStateFlow emits later on.
|
||||||
|
vaultRepository.sync()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleSearchIconClick() {
|
||||||
|
sendEvent(
|
||||||
|
event = VerificationCodeEvent.NavigateToVaultSearchScreen,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleSyncClick() {
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(
|
||||||
|
dialogState = VerificationCodeState.DialogState.Loading(
|
||||||
|
message = R.string.syncing.asText(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
vaultRepository.sync()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleInternalAction(action: VerificationCodeAction.Internal) {
|
||||||
|
when (action) {
|
||||||
|
is VerificationCodeAction.Internal.IconLoadingSettingReceive ->
|
||||||
|
handleIconsSettingReceived(
|
||||||
|
action,
|
||||||
|
)
|
||||||
|
|
||||||
|
is VerificationCodeAction.Internal.PullToRefreshEnableReceive ->
|
||||||
|
handlePullToRefreshEnableReceive(
|
||||||
|
action,
|
||||||
|
)
|
||||||
|
|
||||||
|
is VerificationCodeAction.Internal.VaultDataReceive -> handleVaultDataReceive(action)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleIconsSettingReceived(
|
||||||
|
action: VerificationCodeAction.Internal.IconLoadingSettingReceive,
|
||||||
|
) {
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(isIconLoadingDisabled = action.isIconLoadingDisabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
vaultRepository.vaultDataStateFlow.value.data?.let { vaultData ->
|
||||||
|
updateStateWithVaultData(vaultData, clearDialogState = false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handlePullToRefreshEnableReceive(
|
||||||
|
action: VerificationCodeAction.Internal.PullToRefreshEnableReceive,
|
||||||
|
) {
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(isPullToRefreshSettingEnabled = action.isPullToRefreshEnabled)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleVaultDataReceive(action: VerificationCodeAction.Internal.VaultDataReceive) {
|
||||||
|
updateViewState(action.vaultData)
|
||||||
|
}
|
||||||
|
//endregion VerificationCode Handlers
|
||||||
|
|
||||||
|
private fun updateViewState(vaultData: DataState<VaultData>) {
|
||||||
|
when (vaultData) {
|
||||||
|
is DataState.Error -> vaultErrorReceive(vaultData)
|
||||||
|
is DataState.Loaded -> vaultLoadedReceive(vaultData = vaultData)
|
||||||
|
is DataState.Loading -> vaultLoadingReceive()
|
||||||
|
is DataState.NoNetwork -> vaultNoNetworkReceive(vaultData)
|
||||||
|
is DataState.Pending -> vaultPendingReceive(vaultData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun vaultNoNetworkReceive(vaultData: DataState.NoNetwork<VaultData>) {
|
||||||
|
if (vaultData.data != null) {
|
||||||
|
updateStateWithVaultData(vaultData = vaultData.data, clearDialogState = true)
|
||||||
|
} else {
|
||||||
|
mutableStateFlow.update { currentState ->
|
||||||
|
currentState.copy(
|
||||||
|
viewState = VerificationCodeState.ViewState.Error(
|
||||||
|
message = R.string.internet_connection_required_title
|
||||||
|
.asText()
|
||||||
|
.concat(R.string.internet_connection_required_message.asText()),
|
||||||
|
),
|
||||||
|
dialogState = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sendEvent(VerificationCodeEvent.DismissPullToRefresh)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun vaultPendingReceive(vaultData: DataState.Pending<VaultData>) {
|
||||||
|
updateStateWithVaultData(vaultData = vaultData.data, clearDialogState = false)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun vaultLoadedReceive(vaultData: DataState.Loaded<VaultData>) {
|
||||||
|
updateStateWithVaultData(vaultData = vaultData.data, clearDialogState = true)
|
||||||
|
sendEvent(VerificationCodeEvent.DismissPullToRefresh)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun vaultLoadingReceive() {
|
||||||
|
mutableStateFlow.update { it.copy(viewState = VerificationCodeState.ViewState.Loading) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun vaultErrorReceive(vaultData: DataState.Error<VaultData>) {
|
||||||
|
if (vaultData.data != null) {
|
||||||
|
updateStateWithVaultData(vaultData = vaultData.data, clearDialogState = true)
|
||||||
|
} else {
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(
|
||||||
|
viewState = VerificationCodeState.ViewState.Error(
|
||||||
|
message = R.string.generic_error_message.asText(),
|
||||||
|
),
|
||||||
|
dialogState = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sendEvent(VerificationCodeEvent.DismissPullToRefresh)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateStateWithVaultData(
|
||||||
|
vaultData: VaultData,
|
||||||
|
clearDialogState: Boolean,
|
||||||
|
) {
|
||||||
|
val viewState = vaultData
|
||||||
|
.cipherViewList
|
||||||
|
.filter {
|
||||||
|
it.type == CipherType.LOGIN &&
|
||||||
|
!it.login?.totp.isNullOrBlank() &&
|
||||||
|
it.deletedDate == null
|
||||||
|
}
|
||||||
|
.toFilteredList(state.vaultFilterType)
|
||||||
|
.toVerificationCodeViewState(
|
||||||
|
baseIconUrl = state.baseIconUrl,
|
||||||
|
isIconLoadingDisabled = state.isIconLoadingDisabled,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (viewState is VerificationCodeState.ViewState.NoItems) {
|
||||||
|
sendEvent(VerificationCodeEvent.NavigateBack)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
mutableStateFlow.update { state ->
|
||||||
|
state.copy(
|
||||||
|
viewState = viewState,
|
||||||
|
dialogState = state.dialogState.takeUnless { clearDialogState },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Models state of the verification code screen.
|
* Models state of the verification code screen.
|
||||||
*
|
|
||||||
* @property viewState indicates what view state the screen is in.
|
|
||||||
*/
|
*/
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class VerificationCodeState(
|
data class VerificationCodeState(
|
||||||
val viewState: ViewState,
|
val viewState: ViewState,
|
||||||
|
val vaultFilterType: VaultFilterType,
|
||||||
|
val isIconLoadingDisabled: Boolean,
|
||||||
|
val baseIconUrl: String,
|
||||||
|
val dialogState: DialogState?,
|
||||||
|
val isPullToRefreshSettingEnabled: Boolean,
|
||||||
) : Parcelable {
|
) : Parcelable {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates that the pull-to-refresh should be enabled in the UI.
|
||||||
|
*/
|
||||||
|
val isPullToRefreshEnabled: Boolean
|
||||||
|
get() = isPullToRefreshSettingEnabled && viewState.isPullToRefreshEnabled
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents the current state of any dialogs on the screen.
|
||||||
|
*/
|
||||||
|
sealed class DialogState : Parcelable {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a loading dialog with the given [message].
|
||||||
|
*/
|
||||||
|
@Parcelize
|
||||||
|
data class Loading(
|
||||||
|
val message: Text,
|
||||||
|
) : DialogState()
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents the specific view states for the [VerificationCodeScreen].
|
* Represents the specific view states for the [VerificationCodeScreen].
|
||||||
*/
|
*/
|
||||||
|
@ -60,18 +301,71 @@ data class VerificationCodeState(
|
||||||
sealed class ViewState : Parcelable {
|
sealed class ViewState : Parcelable {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents an empty content state for the [VerificationCodeScreen].
|
* Indicates the pull-to-refresh feature should be available during the current state.
|
||||||
*/
|
*/
|
||||||
@Parcelize
|
abstract val isPullToRefreshEnabled: Boolean
|
||||||
data object Empty : ViewState()
|
|
||||||
|
/**
|
||||||
|
* Represents a state where the [VerificationCodeScreen] has no items to display.
|
||||||
|
*/
|
||||||
|
data object NoItems : ViewState() {
|
||||||
|
override val isPullToRefreshEnabled: Boolean get() = true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loading state for the [VerificationCodeScreen],
|
||||||
|
* signifying that the content is being processed.
|
||||||
|
*/
|
||||||
|
data object Loading : ViewState() {
|
||||||
|
override val isPullToRefreshEnabled: Boolean get() = false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents an error state for the [VerificationCodeScreen].
|
||||||
|
*
|
||||||
|
* @property message Error message to display.
|
||||||
|
*/
|
||||||
|
data class Error(
|
||||||
|
val message: Text,
|
||||||
|
) : ViewState() {
|
||||||
|
override val isPullToRefreshEnabled: Boolean get() = true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Content state for the [VerificationCodeScreen] showing the actual content or items.
|
||||||
|
*/
|
||||||
|
data class Content(
|
||||||
|
val verificationCodeDisplayItems: List<VerificationCodeDisplayItem>,
|
||||||
|
) : ViewState() {
|
||||||
|
override val isPullToRefreshEnabled: Boolean get() = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The data for the verification code item to displayed.
|
||||||
|
*/
|
||||||
|
@Parcelize
|
||||||
|
data class VerificationCodeDisplayItem(
|
||||||
|
val id: String,
|
||||||
|
val label: String,
|
||||||
|
val supportingLabel: String?,
|
||||||
|
val timeLeftSeconds: Int,
|
||||||
|
val periodSeconds: Int,
|
||||||
|
val authCode: String,
|
||||||
|
val startIcon: IconData = IconData.Local(R.drawable.ic_login_item),
|
||||||
|
) : Parcelable
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Models events for the [VerificationCodeScreen].
|
* Models events for the [VerificationCodeScreen].
|
||||||
*/
|
*/
|
||||||
sealed class VerificationCodeEvent {
|
sealed class VerificationCodeEvent {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dismisses the pull-to-refresh indicator.
|
||||||
|
*/
|
||||||
|
data object DismissPullToRefresh : VerificationCodeEvent()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Navigate back.
|
* Navigate back.
|
||||||
*/
|
*/
|
||||||
|
@ -83,6 +377,11 @@ sealed class VerificationCodeEvent {
|
||||||
* @property id the id of the item to navigate to.
|
* @property id the id of the item to navigate to.
|
||||||
*/
|
*/
|
||||||
data class NavigateToVaultItem(val id: String) : VerificationCodeEvent()
|
data class NavigateToVaultItem(val id: String) : VerificationCodeEvent()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Navigates to the VaultSearchScreen.
|
||||||
|
*/
|
||||||
|
data object NavigateToVaultSearchScreen : VerificationCodeEvent()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -91,14 +390,71 @@ sealed class VerificationCodeEvent {
|
||||||
sealed class VerificationCodeAction {
|
sealed class VerificationCodeAction {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Click the back button.
|
* User has clicked the back button.
|
||||||
*/
|
*/
|
||||||
data object BackClick : VerificationCodeAction()
|
data object BackClick : VerificationCodeAction()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User has clicked the copy button.
|
||||||
|
*/
|
||||||
|
data class CopyClick(val text: String) : VerificationCodeAction()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Navigates to an item.
|
* Navigates to an item.
|
||||||
*
|
*
|
||||||
* @property id the id of the item to navigate to.
|
* @property id the id of the item to navigate to.
|
||||||
*/
|
*/
|
||||||
data class ItemClick(val id: String) : VerificationCodeAction()
|
data class ItemClick(val id: String) : VerificationCodeAction()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User has clicked the lock button.
|
||||||
|
*/
|
||||||
|
data object LockClick : VerificationCodeAction()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User has clicked the refresh button.
|
||||||
|
*/
|
||||||
|
data object RefreshClick : VerificationCodeAction()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User has triggered a pull to refresh.
|
||||||
|
*/
|
||||||
|
data object RefreshPull : VerificationCodeAction()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User has clicked the search icon.
|
||||||
|
*/
|
||||||
|
data object SearchIconClick : VerificationCodeAction()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User has clicked the refresh button.
|
||||||
|
*/
|
||||||
|
data object SyncClick : VerificationCodeAction()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Actions for internal use by the ViewModel.
|
||||||
|
*/
|
||||||
|
sealed class Internal : VerificationCodeAction() {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates that the pull to refresh feature toggle has changed.
|
||||||
|
*/
|
||||||
|
data class PullToRefreshEnableReceive(
|
||||||
|
val isPullToRefreshEnabled: Boolean,
|
||||||
|
) : Internal()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates the icon setting was received.
|
||||||
|
*/
|
||||||
|
data class IconLoadingSettingReceive(
|
||||||
|
val isIconLoadingDisabled: Boolean,
|
||||||
|
) : Internal()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates a vault data was received.
|
||||||
|
*/
|
||||||
|
data class VaultDataReceive(
|
||||||
|
val vaultData: DataState<VaultData>,
|
||||||
|
) : Internal()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
package com.x8bit.bitwarden.ui.vault.feature.verificationcode.handlers
|
||||||
|
|
||||||
|
import com.x8bit.bitwarden.ui.vault.feature.verificationcode.VerificationCodeAction
|
||||||
|
import com.x8bit.bitwarden.ui.vault.feature.verificationcode.VerificationCodeViewModel
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A collection of handler functions for managing actions within the context of viewing a list of
|
||||||
|
* verification code items.
|
||||||
|
*/
|
||||||
|
data class VerificationCodeHandlers(
|
||||||
|
val backClick: () -> Unit,
|
||||||
|
val searchIconClick: () -> Unit,
|
||||||
|
val itemClick: (id: String) -> Unit,
|
||||||
|
val refreshClick: () -> Unit,
|
||||||
|
val syncClick: () -> Unit,
|
||||||
|
val lockClick: () -> Unit,
|
||||||
|
val copyClick: (text: String) -> Unit,
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
/**
|
||||||
|
* Creates an instance of [VerificationCodeHandlers] by binding actions to the provided
|
||||||
|
* [VerificationCodeViewModel].
|
||||||
|
*/
|
||||||
|
fun create(
|
||||||
|
viewModel: VerificationCodeViewModel,
|
||||||
|
): VerificationCodeHandlers =
|
||||||
|
VerificationCodeHandlers(
|
||||||
|
backClick = { viewModel.trySendAction(VerificationCodeAction.BackClick) },
|
||||||
|
searchIconClick = {
|
||||||
|
viewModel.trySendAction(VerificationCodeAction.SearchIconClick)
|
||||||
|
},
|
||||||
|
itemClick = { viewModel.trySendAction(VerificationCodeAction.ItemClick(it)) },
|
||||||
|
refreshClick = { viewModel.trySendAction(VerificationCodeAction.RefreshClick) },
|
||||||
|
syncClick = { viewModel.trySendAction(VerificationCodeAction.SyncClick) },
|
||||||
|
lockClick = { viewModel.trySendAction(VerificationCodeAction.LockClick) },
|
||||||
|
copyClick = { viewModel.trySendAction(VerificationCodeAction.CopyClick(it)) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,55 @@
|
||||||
|
package com.x8bit.bitwarden.ui.vault.feature.verificationcode.util
|
||||||
|
|
||||||
|
import com.bitwarden.core.CipherView
|
||||||
|
import com.x8bit.bitwarden.ui.vault.feature.vault.util.toLoginIconData
|
||||||
|
import com.x8bit.bitwarden.ui.vault.feature.verificationcode.VerificationCodeDisplayItem
|
||||||
|
import com.x8bit.bitwarden.ui.vault.feature.verificationcode.VerificationCodeState
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a list of [CipherView] to a list of [VerificationCodeDisplayItem].
|
||||||
|
*/
|
||||||
|
fun List<CipherView>.toVerificationCodeViewState(
|
||||||
|
baseIconUrl: String,
|
||||||
|
isIconLoadingDisabled: Boolean,
|
||||||
|
): VerificationCodeState.ViewState =
|
||||||
|
if (isNotEmpty()) {
|
||||||
|
VerificationCodeState.ViewState.Content(
|
||||||
|
verificationCodeDisplayItems = toDisplayItemList(
|
||||||
|
baseIconUrl = baseIconUrl,
|
||||||
|
isIconLoadingDisabled = isIconLoadingDisabled,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
VerificationCodeState.ViewState.NoItems
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun List<CipherView>.toDisplayItemList(
|
||||||
|
baseIconUrl: String,
|
||||||
|
isIconLoadingDisabled: Boolean,
|
||||||
|
): List<VerificationCodeDisplayItem> =
|
||||||
|
this.map {
|
||||||
|
it.toDisplayItem(
|
||||||
|
baseIconUrl = baseIconUrl,
|
||||||
|
isIconLoadingDisabled = isIconLoadingDisabled,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A function used to create a sample [VerificationCodeDisplayItem].
|
||||||
|
*/
|
||||||
|
fun CipherView.toDisplayItem(
|
||||||
|
baseIconUrl: String,
|
||||||
|
isIconLoadingDisabled: Boolean,
|
||||||
|
): VerificationCodeDisplayItem =
|
||||||
|
VerificationCodeDisplayItem(
|
||||||
|
id = id.orEmpty(),
|
||||||
|
authCode = "123456",
|
||||||
|
label = name,
|
||||||
|
supportingLabel = login?.username,
|
||||||
|
periodSeconds = 30,
|
||||||
|
timeLeftSeconds = 15,
|
||||||
|
startIcon = login?.uris.toLoginIconData(
|
||||||
|
baseIconUrl = baseIconUrl,
|
||||||
|
isIconLoadingDisabled = isIconLoadingDisabled,
|
||||||
|
),
|
||||||
|
)
|
|
@ -0,0 +1,355 @@
|
||||||
|
package com.x8bit.bitwarden.ui.vault.feature.verificationcode
|
||||||
|
|
||||||
|
import androidx.compose.ui.test.assert
|
||||||
|
import androidx.compose.ui.test.assertIsDisplayed
|
||||||
|
import androidx.compose.ui.test.assertIsNotDisplayed
|
||||||
|
import androidx.compose.ui.test.assertTextEquals
|
||||||
|
import androidx.compose.ui.test.filterToOne
|
||||||
|
import androidx.compose.ui.test.hasAnyAncestor
|
||||||
|
import androidx.compose.ui.test.hasScrollToNodeAction
|
||||||
|
import androidx.compose.ui.test.hasText
|
||||||
|
import androidx.compose.ui.test.isDialog
|
||||||
|
import androidx.compose.ui.test.isDisplayed
|
||||||
|
import androidx.compose.ui.test.isPopup
|
||||||
|
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.performScrollToNode
|
||||||
|
import com.x8bit.bitwarden.data.platform.repository.model.Environment
|
||||||
|
import com.x8bit.bitwarden.data.platform.repository.util.baseIconUrl
|
||||||
|
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
|
||||||
|
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
|
||||||
|
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||||
|
import com.x8bit.bitwarden.ui.vault.feature.vault.model.VaultFilterType
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
import io.mockk.verify
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
|
import org.junit.jupiter.api.Assertions.assertTrue
|
||||||
|
|
||||||
|
class VerificationCodeScreenTest : BaseComposeTest() {
|
||||||
|
|
||||||
|
private var onNavigateBackCalled = false
|
||||||
|
private var onNavigateToVaultItemId: String? = null
|
||||||
|
|
||||||
|
private val mutableEventFlow = bufferedMutableSharedFlow<VerificationCodeEvent>()
|
||||||
|
private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE)
|
||||||
|
private val viewModel = mockk<VerificationCodeViewModel>(relaxed = true) {
|
||||||
|
every { eventFlow } returns mutableEventFlow
|
||||||
|
every { stateFlow } returns mutableStateFlow
|
||||||
|
}
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
composeTestRule.setContent {
|
||||||
|
VerificationCodeScreen(
|
||||||
|
viewModel = viewModel,
|
||||||
|
onNavigateBack = { onNavigateBackCalled = true },
|
||||||
|
onNavigateToVaultItemScreen = { onNavigateToVaultItemId = it },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `NavigateBack event should invoke onNavigateBack`() {
|
||||||
|
mutableEventFlow.tryEmit(VerificationCodeEvent.NavigateBack)
|
||||||
|
assertTrue(onNavigateBackCalled)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `NavigateToVaultItem event should call onNavigateToVaultItemScreen`() {
|
||||||
|
val id = "id4321"
|
||||||
|
mutableEventFlow.tryEmit(VerificationCodeEvent.NavigateToVaultItem(id = id))
|
||||||
|
assertEquals(id, onNavigateToVaultItemId)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `clicking back button should send BackClick action`() {
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithContentDescription(label = "Back")
|
||||||
|
.performClick()
|
||||||
|
|
||||||
|
verify { viewModel.trySendAction(VerificationCodeAction.BackClick) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `search icon click should send SearchIconClick action`() {
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithContentDescription("Search vault")
|
||||||
|
.performClick()
|
||||||
|
|
||||||
|
verify { viewModel.trySendAction(VerificationCodeAction.SearchIconClick) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `refresh button click should send RefreshClick action`() {
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(viewState = VerificationCodeState.ViewState.Error(message = "".asText()))
|
||||||
|
}
|
||||||
|
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithText("Try again")
|
||||||
|
.performClick()
|
||||||
|
|
||||||
|
verify { viewModel.trySendAction(VerificationCodeAction.RefreshClick) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `error text and retry should be displayed according to state`() {
|
||||||
|
val message = "error_message"
|
||||||
|
mutableStateFlow.update { DEFAULT_STATE }
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithText(message)
|
||||||
|
.assertIsNotDisplayed()
|
||||||
|
|
||||||
|
mutableStateFlow.update { it.copy(viewState = VerificationCodeState.ViewState.NoItems) }
|
||||||
|
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithText(message)
|
||||||
|
.assertIsNotDisplayed()
|
||||||
|
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(viewState = VerificationCodeState.ViewState.Error(message.asText()))
|
||||||
|
}
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithText(message)
|
||||||
|
.assertIsDisplayed()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Items text should be displayed according to state`() {
|
||||||
|
val items = "Items"
|
||||||
|
mutableStateFlow.update { DEFAULT_STATE }
|
||||||
|
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithText(text = items)
|
||||||
|
.assertDoesNotExist()
|
||||||
|
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(
|
||||||
|
viewState = VerificationCodeState.ViewState.Content(
|
||||||
|
verificationCodeDisplayItems = listOf(
|
||||||
|
createDisplayItem(1),
|
||||||
|
createDisplayItem(2),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composeTestRule
|
||||||
|
.onNode(hasScrollToNodeAction())
|
||||||
|
.performScrollToNode(hasText(items))
|
||||||
|
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithText(text = items)
|
||||||
|
.assertIsDisplayed()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Items text count should be displayed according to state`() {
|
||||||
|
val items = "Items"
|
||||||
|
mutableStateFlow.update { DEFAULT_STATE }
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithText(text = items)
|
||||||
|
.assertDoesNotExist()
|
||||||
|
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(
|
||||||
|
viewState = VerificationCodeState.ViewState.Content(
|
||||||
|
verificationCodeDisplayItems = listOf(
|
||||||
|
createDisplayItem(number = 1),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composeTestRule
|
||||||
|
.onNode(hasScrollToNodeAction())
|
||||||
|
.performScrollToNode(hasText(items))
|
||||||
|
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithText(text = items)
|
||||||
|
.assertIsDisplayed()
|
||||||
|
.assertTextEquals(items, "1")
|
||||||
|
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(
|
||||||
|
viewState = VerificationCodeState.ViewState.Content(
|
||||||
|
verificationCodeDisplayItems = listOf(
|
||||||
|
createDisplayItem(number = 1),
|
||||||
|
createDisplayItem(number = 2),
|
||||||
|
createDisplayItem(number = 3),
|
||||||
|
createDisplayItem(number = 4),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composeTestRule
|
||||||
|
.onNode(hasScrollToNodeAction())
|
||||||
|
.performScrollToNode(hasText(items))
|
||||||
|
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithText(text = items)
|
||||||
|
.assertIsDisplayed()
|
||||||
|
.assertTextEquals(items, "4")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `displayItems should be displayed according to state`() {
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(
|
||||||
|
viewState = VerificationCodeState.ViewState.Content(
|
||||||
|
verificationCodeDisplayItems = listOf(
|
||||||
|
createDisplayItem(number = 1),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithText(text = "1")
|
||||||
|
.assertIsDisplayed()
|
||||||
|
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithText(text = "Label 1")
|
||||||
|
.assertIsDisplayed()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `clicking on a display item should send ItemClick action`() {
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(
|
||||||
|
viewState = VerificationCodeState.ViewState.Content(
|
||||||
|
verificationCodeDisplayItems = listOf(
|
||||||
|
createDisplayItem(number = 1),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithText(text = "Label 1")
|
||||||
|
.assertIsDisplayed()
|
||||||
|
.performClick()
|
||||||
|
|
||||||
|
verify {
|
||||||
|
viewModel.trySendAction(VerificationCodeAction.ItemClick("1"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `clicking on copy button should send CopyClick action`() {
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(
|
||||||
|
viewState = VerificationCodeState.ViewState.Content(
|
||||||
|
verificationCodeDisplayItems = listOf(
|
||||||
|
createDisplayItem(number = 1),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithContentDescription(label = "Copy")
|
||||||
|
.assertIsDisplayed()
|
||||||
|
.performClick()
|
||||||
|
|
||||||
|
verify {
|
||||||
|
viewModel.trySendAction(VerificationCodeAction.CopyClick("123456"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `on overflow item click should display menu`() {
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithContentDescription(label = "More")
|
||||||
|
.performClick()
|
||||||
|
|
||||||
|
composeTestRule
|
||||||
|
.onAllNodesWithText(text = "Sync")
|
||||||
|
.filterToOne(hasAnyAncestor(isPopup()))
|
||||||
|
.isDisplayed()
|
||||||
|
|
||||||
|
composeTestRule
|
||||||
|
.onAllNodesWithText(text = "Lock")
|
||||||
|
.filterToOne(hasAnyAncestor(isPopup()))
|
||||||
|
.isDisplayed()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `on sync click should send SyncClick`() {
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithContentDescription(label = "More")
|
||||||
|
.performClick()
|
||||||
|
|
||||||
|
composeTestRule
|
||||||
|
.onAllNodesWithText(text = "Sync")
|
||||||
|
.filterToOne(hasAnyAncestor(isPopup()))
|
||||||
|
.performClick()
|
||||||
|
|
||||||
|
verify {
|
||||||
|
viewModel.trySendAction(VerificationCodeAction.SyncClick)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `on lock click should send LockClick`() {
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithContentDescription(label = "More")
|
||||||
|
.performClick()
|
||||||
|
|
||||||
|
composeTestRule
|
||||||
|
.onAllNodesWithText(text = "Lock")
|
||||||
|
.filterToOne(hasAnyAncestor(isPopup()))
|
||||||
|
.performClick()
|
||||||
|
|
||||||
|
verify {
|
||||||
|
viewModel.trySendAction(VerificationCodeAction.LockClick)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `loading dialog should be displayed according to state`() {
|
||||||
|
val loadingMessage = "syncing"
|
||||||
|
composeTestRule.onNode(isDialog()).assertDoesNotExist()
|
||||||
|
composeTestRule.onNodeWithText(loadingMessage).assertDoesNotExist()
|
||||||
|
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(
|
||||||
|
dialogState = VerificationCodeState.DialogState.Loading(
|
||||||
|
message = loadingMessage.asText(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithText(loadingMessage)
|
||||||
|
.assertIsDisplayed()
|
||||||
|
.assert(hasAnyAncestor(isDialog()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createDisplayItem(number: Int): VerificationCodeDisplayItem =
|
||||||
|
VerificationCodeDisplayItem(
|
||||||
|
id = number.toString(),
|
||||||
|
authCode = "123456",
|
||||||
|
label = "Label $number",
|
||||||
|
supportingLabel = "Supporting Label $number",
|
||||||
|
periodSeconds = 30,
|
||||||
|
timeLeftSeconds = 15,
|
||||||
|
)
|
||||||
|
|
||||||
|
private val DEFAULT_STATE = VerificationCodeState(
|
||||||
|
viewState = VerificationCodeState.ViewState.Loading,
|
||||||
|
vaultFilterType = VaultFilterType.AllVaults,
|
||||||
|
isIconLoadingDisabled = false,
|
||||||
|
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
|
||||||
|
isPullToRefreshSettingEnabled = false,
|
||||||
|
dialogState = null,
|
||||||
|
)
|
|
@ -1,16 +1,83 @@
|
||||||
package com.x8bit.bitwarden.ui.vault.feature.verificationcode
|
package com.x8bit.bitwarden.ui.vault.feature.verificationcode
|
||||||
|
|
||||||
import androidx.lifecycle.SavedStateHandle
|
import android.net.Uri
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
|
import com.x8bit.bitwarden.R
|
||||||
|
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager
|
||||||
|
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
||||||
|
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||||
|
import com.x8bit.bitwarden.data.platform.repository.model.DataState
|
||||||
|
import com.x8bit.bitwarden.data.platform.repository.model.Environment
|
||||||
|
import com.x8bit.bitwarden.data.platform.repository.util.baseIconUrl
|
||||||
|
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCipherView
|
||||||
|
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCollectionView
|
||||||
|
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockFolderView
|
||||||
|
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSendView
|
||||||
|
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||||
|
import com.x8bit.bitwarden.data.vault.repository.model.VaultData
|
||||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
|
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
|
||||||
|
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||||
|
import com.x8bit.bitwarden.ui.platform.base.util.concat
|
||||||
|
import com.x8bit.bitwarden.ui.vault.feature.vault.model.VaultFilterType
|
||||||
|
import com.x8bit.bitwarden.ui.vault.feature.verificationcode.util.toDisplayItem
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.just
|
||||||
|
import io.mockk.mockk
|
||||||
|
import io.mockk.mockkStatic
|
||||||
|
import io.mockk.runs
|
||||||
|
import io.mockk.unmockkStatic
|
||||||
|
import io.mockk.verify
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.jupiter.api.AfterEach
|
||||||
import org.junit.jupiter.api.Assertions.assertEquals
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
|
import org.junit.jupiter.api.Assertions.assertFalse
|
||||||
|
import org.junit.jupiter.api.Assertions.assertTrue
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
|
|
||||||
class VerificationCodeViewModelTest : BaseViewModelTest() {
|
class VerificationCodeViewModelTest : BaseViewModelTest() {
|
||||||
|
|
||||||
|
private val clipboardManager: BitwardenClipboardManager = mockk()
|
||||||
|
|
||||||
|
private val mutableVaultDataStateFlow =
|
||||||
|
MutableStateFlow<DataState<VaultData>>(DataState.Loading)
|
||||||
|
private val vaultRepository: VaultRepository = mockk {
|
||||||
|
every { vaultFilterType } returns VaultFilterType.AllVaults
|
||||||
|
every { vaultDataStateFlow } returns mutableVaultDataStateFlow
|
||||||
|
every { sync() } just runs
|
||||||
|
}
|
||||||
|
|
||||||
|
private val environmentRepository: EnvironmentRepository = mockk {
|
||||||
|
every { environment } returns Environment.Us
|
||||||
|
every { environmentStateFlow } returns mockk()
|
||||||
|
}
|
||||||
|
|
||||||
|
private val mutablePullToRefreshEnabledFlow = MutableStateFlow(false)
|
||||||
|
private val mutableIsIconLoadingDisabledFlow = MutableStateFlow(false)
|
||||||
|
private val settingsRepository: SettingsRepository = mockk {
|
||||||
|
every { isIconLoadingDisabled } returns false
|
||||||
|
every { isIconLoadingDisabledFlow } returns mutableIsIconLoadingDisabledFlow
|
||||||
|
every { getPullToRefreshEnabledFlow() } returns mutablePullToRefreshEnabledFlow
|
||||||
|
}
|
||||||
|
private val initialState = createVerificationCodeState()
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
fun tearDown() {
|
||||||
|
unmockkStatic(Uri::class)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `on BackClick should emit NavigateBack`() = runTest {
|
fun `initial state should be correct`() = runTest {
|
||||||
|
val viewModel = createViewModel()
|
||||||
|
viewModel.stateFlow.test {
|
||||||
|
assertEquals(
|
||||||
|
initialState, awaitItem(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `on BackClick should emit onNavigateBack`() = runTest {
|
||||||
val viewModel = createViewModel()
|
val viewModel = createViewModel()
|
||||||
viewModel.eventFlow.test {
|
viewModel.eventFlow.test {
|
||||||
viewModel.trySendAction(VerificationCodeAction.BackClick)
|
viewModel.trySendAction(VerificationCodeAction.BackClick)
|
||||||
|
@ -18,6 +85,19 @@ class VerificationCodeViewModelTest : BaseViewModelTest() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `onCopyClick should call setText on the ClipboardManager`() {
|
||||||
|
val authCode = "123456"
|
||||||
|
val viewModel = createViewModel()
|
||||||
|
every { clipboardManager.setText(text = authCode) } just runs
|
||||||
|
|
||||||
|
viewModel.trySendAction(VerificationCodeAction.CopyClick(authCode))
|
||||||
|
|
||||||
|
verify(exactly = 1) {
|
||||||
|
clipboardManager.setText(text = authCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `on ItemClick should emit ItemClick`() = runTest {
|
fun `on ItemClick should emit ItemClick`() = runTest {
|
||||||
val viewModel = createViewModel()
|
val viewModel = createViewModel()
|
||||||
|
@ -29,13 +109,484 @@ class VerificationCodeViewModelTest : BaseViewModelTest() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createViewModel(
|
@Test
|
||||||
state: VerificationCodeState? = DEFAULT_STATE,
|
fun `SearchIconClick should emit NavigateToVaultSearchScreen`() = runTest {
|
||||||
): VerificationCodeViewModel = VerificationCodeViewModel(
|
val viewModel = createViewModel()
|
||||||
savedStateHandle = SavedStateHandle().apply { set("state", state) },
|
viewModel.eventFlow.test {
|
||||||
|
viewModel.actionChannel.trySend(VerificationCodeAction.SearchIconClick)
|
||||||
|
assertEquals(VerificationCodeEvent.NavigateToVaultSearchScreen, awaitItem())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `LockClick should call lockVaultForCurrentUser`() {
|
||||||
|
every { vaultRepository.lockVaultForCurrentUser() } just runs
|
||||||
|
val viewModel = createViewModel()
|
||||||
|
|
||||||
|
viewModel.trySendAction(VerificationCodeAction.LockClick)
|
||||||
|
|
||||||
|
verify(exactly = 1) {
|
||||||
|
vaultRepository.lockVaultForCurrentUser()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `SyncClick should display the loading dialog and call sync`() {
|
||||||
|
val viewModel = createViewModel()
|
||||||
|
|
||||||
|
viewModel.trySendAction(VerificationCodeAction.SyncClick)
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
initialState.copy(
|
||||||
|
dialogState = VerificationCodeState.DialogState.Loading(
|
||||||
|
message = R.string.syncing.asText(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
viewModel.stateFlow.value,
|
||||||
|
)
|
||||||
|
verify(exactly = 1) {
|
||||||
|
vaultRepository.sync()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `ItemClick for vault item should emit NavigateToVaultItem`() = runTest {
|
||||||
|
val viewModel = createViewModel()
|
||||||
|
viewModel.eventFlow.test {
|
||||||
|
viewModel.actionChannel.trySend(VerificationCodeAction.ItemClick(id = "mock"))
|
||||||
|
assertEquals(VerificationCodeEvent.NavigateToVaultItem(id = "mock"), awaitItem())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `RefreshClick should sync`() = runTest {
|
||||||
|
val viewModel = createViewModel()
|
||||||
|
viewModel.actionChannel.trySend(VerificationCodeAction.RefreshClick)
|
||||||
|
verify { vaultRepository.sync() }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `vaultDataStateFlow Pending with data should update state to Content`() = runTest {
|
||||||
|
setupMockUri()
|
||||||
|
|
||||||
|
mutableVaultDataStateFlow.tryEmit(
|
||||||
|
value = DataState.Pending(
|
||||||
|
data = VaultData(
|
||||||
|
cipherViewList = listOf(createMockCipherView(number = 1, isDeleted = false)),
|
||||||
|
folderViewList = listOf(createMockFolderView(number = 1)),
|
||||||
|
collectionViewList = listOf(createMockCollectionView(number = 1)),
|
||||||
|
sendViewList = listOf(createMockSendView(number = 1)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
val viewModel = createViewModel()
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
createVerificationCodeState(
|
||||||
|
viewState = VerificationCodeState.ViewState.Content(
|
||||||
|
verificationCodeDisplayItems = listOf(
|
||||||
|
createMockCipherView(
|
||||||
|
number = 1,
|
||||||
|
isDeleted = false,
|
||||||
|
)
|
||||||
|
.toDisplayItem(
|
||||||
|
baseIconUrl = initialState.baseIconUrl,
|
||||||
|
isIconLoadingDisabled = initialState.isIconLoadingDisabled,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
viewModel.stateFlow.value,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("MaxLineLength")
|
||||||
|
@Test
|
||||||
|
fun `vaultDataStateFlow Pending with empty data should call NavigateBack to go to the vault screen`() =
|
||||||
|
runTest {
|
||||||
|
val dataState = DataState.Pending(
|
||||||
|
data = VaultData(
|
||||||
|
cipherViewList = listOf(createMockCipherView(number = 1)),
|
||||||
|
folderViewList = listOf(createMockFolderView(number = 1)),
|
||||||
|
collectionViewList = listOf(createMockCollectionView(number = 1)),
|
||||||
|
sendViewList = listOf(createMockSendView(number = 1)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
val viewModel = createViewModel()
|
||||||
|
|
||||||
|
viewModel.eventFlow.test {
|
||||||
|
mutableVaultDataStateFlow.tryEmit(value = dataState)
|
||||||
|
assertEquals(VerificationCodeEvent.NavigateBack, awaitItem())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `vaultDataStateFlow Pending with trash data should call NavigateBack event`() = runTest {
|
||||||
|
val dataState = DataState.Pending(
|
||||||
|
data = VaultData(
|
||||||
|
cipherViewList = listOf(createMockCipherView(number = 1)),
|
||||||
|
folderViewList = listOf(createMockFolderView(number = 1)),
|
||||||
|
collectionViewList = listOf(createMockCollectionView(number = 1)),
|
||||||
|
sendViewList = listOf(createMockSendView(number = 1)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
val viewModel = createViewModel()
|
||||||
|
|
||||||
|
viewModel.eventFlow.test {
|
||||||
|
mutableVaultDataStateFlow.tryEmit(value = dataState)
|
||||||
|
assertEquals(VerificationCodeEvent.NavigateBack, awaitItem())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `vaultDataStateFlow Error without data should update state to Error`() = runTest {
|
||||||
|
val dataState = DataState.Error<VaultData>(
|
||||||
|
error = IllegalStateException(),
|
||||||
|
)
|
||||||
|
|
||||||
|
val viewModel = createViewModel()
|
||||||
|
|
||||||
|
viewModel.eventFlow.test {
|
||||||
|
mutableVaultDataStateFlow.tryEmit(value = dataState)
|
||||||
|
assertEquals(VerificationCodeEvent.DismissPullToRefresh, awaitItem())
|
||||||
|
}
|
||||||
|
assertEquals(
|
||||||
|
createVerificationCodeState(
|
||||||
|
viewState = VerificationCodeState.ViewState.Error(
|
||||||
|
message = R.string.generic_error_message.asText(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
viewModel.stateFlow.value,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `vaultDataStateFlow Error with data should update state to Content`() = runTest {
|
||||||
|
setupMockUri()
|
||||||
|
|
||||||
|
val dataState = DataState.Error(
|
||||||
|
data = VaultData(
|
||||||
|
cipherViewList = listOf(createMockCipherView(number = 1, isDeleted = false)),
|
||||||
|
folderViewList = listOf(createMockFolderView(number = 1)),
|
||||||
|
collectionViewList = listOf(createMockCollectionView(number = 1)),
|
||||||
|
sendViewList = listOf(createMockSendView(number = 1)),
|
||||||
|
),
|
||||||
|
error = IllegalStateException(),
|
||||||
|
)
|
||||||
|
|
||||||
|
val viewModel = createViewModel()
|
||||||
|
|
||||||
|
viewModel.eventFlow.test {
|
||||||
|
mutableVaultDataStateFlow.tryEmit(value = dataState)
|
||||||
|
assertEquals(VerificationCodeEvent.DismissPullToRefresh, awaitItem())
|
||||||
|
}
|
||||||
|
assertEquals(
|
||||||
|
createVerificationCodeState(
|
||||||
|
viewState = VerificationCodeState.ViewState.Content(
|
||||||
|
verificationCodeDisplayItems = listOf(
|
||||||
|
createMockCipherView(
|
||||||
|
number = 1,
|
||||||
|
isDeleted = false,
|
||||||
|
)
|
||||||
|
.toDisplayItem(
|
||||||
|
baseIconUrl = initialState.baseIconUrl,
|
||||||
|
isIconLoadingDisabled = initialState.isIconLoadingDisabled,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
viewModel.stateFlow.value,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("MaxLineLength")
|
||||||
|
@Test
|
||||||
|
fun `vaultDataStateFlow Error with empty data should call NavigateBack to go the vault screen`() =
|
||||||
|
runTest {
|
||||||
|
val dataState = DataState.Error(
|
||||||
|
data = VaultData(
|
||||||
|
cipherViewList = emptyList(),
|
||||||
|
folderViewList = emptyList(),
|
||||||
|
collectionViewList = emptyList(),
|
||||||
|
sendViewList = emptyList(),
|
||||||
|
),
|
||||||
|
error = IllegalStateException(),
|
||||||
|
)
|
||||||
|
|
||||||
|
val viewModel = createViewModel()
|
||||||
|
|
||||||
|
viewModel.eventFlow.test {
|
||||||
|
mutableVaultDataStateFlow.tryEmit(value = dataState)
|
||||||
|
assertEquals(VerificationCodeEvent.NavigateBack, awaitItem())
|
||||||
|
assertEquals(VerificationCodeEvent.DismissPullToRefresh, awaitItem())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("MaxLineLength")
|
||||||
|
@Test
|
||||||
|
fun `vaultDataStateFlow Error with trash data should call NavigateBack to go to the vault screen`() =
|
||||||
|
runTest {
|
||||||
|
val dataState = DataState.Error(
|
||||||
|
data = VaultData(
|
||||||
|
cipherViewList = listOf(createMockCipherView(number = 1, isDeleted = true)),
|
||||||
|
folderViewList = listOf(createMockFolderView(number = 1)),
|
||||||
|
collectionViewList = listOf(createMockCollectionView(number = 1)),
|
||||||
|
sendViewList = listOf(createMockSendView(number = 1)),
|
||||||
|
),
|
||||||
|
error = IllegalStateException(),
|
||||||
|
)
|
||||||
|
|
||||||
|
val viewModel = createViewModel()
|
||||||
|
|
||||||
|
viewModel.eventFlow.test {
|
||||||
|
mutableVaultDataStateFlow.tryEmit(value = dataState)
|
||||||
|
assertEquals(VerificationCodeEvent.NavigateBack, awaitItem())
|
||||||
|
assertEquals(VerificationCodeEvent.DismissPullToRefresh, awaitItem())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `vaultDataStateFlow NoNetwork without data should update state to Error`() = runTest {
|
||||||
|
val dataState = DataState.NoNetwork<VaultData>()
|
||||||
|
|
||||||
|
val viewModel = createViewModel()
|
||||||
|
|
||||||
|
viewModel.eventFlow.test {
|
||||||
|
mutableVaultDataStateFlow.tryEmit(value = dataState)
|
||||||
|
assertEquals(VerificationCodeEvent.DismissPullToRefresh, awaitItem())
|
||||||
|
}
|
||||||
|
assertEquals(
|
||||||
|
createVerificationCodeState(
|
||||||
|
viewState = VerificationCodeState.ViewState.Error(
|
||||||
|
message = R.string.internet_connection_required_title
|
||||||
|
.asText()
|
||||||
|
.concat(R.string.internet_connection_required_message.asText()),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
viewModel.stateFlow.value,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `vaultDataStateFlow NoNetwork with data should update state to Content`() = runTest {
|
||||||
|
setupMockUri()
|
||||||
|
|
||||||
|
val dataState = DataState.NoNetwork(
|
||||||
|
data = VaultData(
|
||||||
|
cipherViewList = listOf(createMockCipherView(number = 1, isDeleted = false)),
|
||||||
|
folderViewList = listOf(createMockFolderView(number = 1)),
|
||||||
|
collectionViewList = listOf(createMockCollectionView(number = 1)),
|
||||||
|
sendViewList = listOf(createMockSendView(number = 1)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
val viewModel = createViewModel()
|
||||||
|
|
||||||
|
viewModel.eventFlow.test {
|
||||||
|
mutableVaultDataStateFlow.tryEmit(value = dataState)
|
||||||
|
assertEquals(VerificationCodeEvent.DismissPullToRefresh, awaitItem())
|
||||||
|
}
|
||||||
|
assertEquals(
|
||||||
|
createVerificationCodeState(
|
||||||
|
viewState = VerificationCodeState.ViewState.Content(
|
||||||
|
verificationCodeDisplayItems = listOf(
|
||||||
|
createMockCipherView(
|
||||||
|
number = 1,
|
||||||
|
isDeleted = false,
|
||||||
|
)
|
||||||
|
.toDisplayItem(
|
||||||
|
baseIconUrl = initialState.baseIconUrl,
|
||||||
|
isIconLoadingDisabled = initialState.isIconLoadingDisabled,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
viewModel.stateFlow.value,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `vaultDataStateFlow NoNetwork with trash data should call NavigateBack`() = runTest {
|
||||||
|
val dataState = DataState.NoNetwork(
|
||||||
|
data = VaultData(
|
||||||
|
cipherViewList = listOf(createMockCipherView(number = 1, isDeleted = true)),
|
||||||
|
folderViewList = listOf(createMockFolderView(number = 1)),
|
||||||
|
collectionViewList = listOf(createMockCollectionView(number = 1)),
|
||||||
|
sendViewList = listOf(createMockSendView(number = 1)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
val viewModel = createViewModel()
|
||||||
|
|
||||||
|
viewModel.eventFlow.test {
|
||||||
|
mutableVaultDataStateFlow.tryEmit(value = dataState)
|
||||||
|
assertEquals(VerificationCodeEvent.NavigateBack, awaitItem())
|
||||||
|
assertEquals(VerificationCodeEvent.DismissPullToRefresh, awaitItem())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("MaxLineLength")
|
||||||
|
@Test
|
||||||
|
fun `vaultDataStateFlow Loaded with empty items should update call NavigateBack to go the vault screen`() =
|
||||||
|
runTest {
|
||||||
|
val dataState = DataState.Loaded(
|
||||||
|
data = VaultData(
|
||||||
|
cipherViewList = emptyList(),
|
||||||
|
folderViewList = emptyList(),
|
||||||
|
collectionViewList = emptyList(),
|
||||||
|
sendViewList = emptyList(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
val viewModel = createViewModel()
|
||||||
|
viewModel.eventFlow.test {
|
||||||
|
mutableVaultDataStateFlow.tryEmit(value = dataState)
|
||||||
|
assertEquals(VerificationCodeEvent.NavigateBack, awaitItem())
|
||||||
|
assertEquals(VerificationCodeEvent.DismissPullToRefresh, awaitItem())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("MaxLineLength")
|
||||||
|
@Test
|
||||||
|
fun `vaultDataStateFlow Loaded with trash items should call NavigateBack to go to the vault screen`() =
|
||||||
|
runTest {
|
||||||
|
val dataState = DataState.Loaded(
|
||||||
|
data = VaultData(
|
||||||
|
cipherViewList = listOf(createMockCipherView(number = 1)),
|
||||||
|
folderViewList = listOf(createMockFolderView(number = 1)),
|
||||||
|
collectionViewList = listOf(createMockCollectionView(number = 1)),
|
||||||
|
sendViewList = listOf(createMockSendView(number = 1)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
val viewModel = createViewModel()
|
||||||
|
|
||||||
|
viewModel.eventFlow.test {
|
||||||
|
mutableVaultDataStateFlow.tryEmit(value = dataState)
|
||||||
|
assertEquals(VerificationCodeEvent.NavigateBack, awaitItem())
|
||||||
|
assertEquals(VerificationCodeEvent.DismissPullToRefresh, awaitItem())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `vaultDataStateFlow Loaded with items should update ViewState to Content`() =
|
||||||
|
runTest {
|
||||||
|
setupMockUri()
|
||||||
|
val dataState = DataState.Loaded(
|
||||||
|
data = VaultData(
|
||||||
|
cipherViewList = listOf(
|
||||||
|
createMockCipherView(
|
||||||
|
number = 1,
|
||||||
|
isDeleted = false,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
folderViewList = listOf(createMockFolderView(number = 1)),
|
||||||
|
collectionViewList = listOf(createMockCollectionView(number = 1)),
|
||||||
|
sendViewList = listOf(createMockSendView(number = 1)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
val viewModel = createViewModel()
|
||||||
|
|
||||||
|
viewModel.eventFlow.test {
|
||||||
|
mutableVaultDataStateFlow.tryEmit(value = dataState)
|
||||||
|
assertEquals(VerificationCodeEvent.DismissPullToRefresh, awaitItem())
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
createVerificationCodeState(
|
||||||
|
viewState = VerificationCodeState.ViewState.Content(
|
||||||
|
listOf(
|
||||||
|
createMockCipherView(
|
||||||
|
number = 1,
|
||||||
|
isDeleted = false,
|
||||||
|
)
|
||||||
|
.toDisplayItem(
|
||||||
|
baseIconUrl = initialState.baseIconUrl,
|
||||||
|
isIconLoadingDisabled = initialState.isIconLoadingDisabled,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
viewModel.stateFlow.value,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `vaultDataStateFlow Loading should update state to Loading`() = runTest {
|
||||||
|
mutableVaultDataStateFlow.tryEmit(value = DataState.Loading)
|
||||||
|
|
||||||
|
val viewModel = createViewModel()
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
createVerificationCodeState(viewState = VerificationCodeState.ViewState.Loading),
|
||||||
|
viewModel.stateFlow.value,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `icon loading state updates should update isIconLoadingDisabled`() = runTest {
|
||||||
|
val viewModel = createViewModel()
|
||||||
|
|
||||||
|
assertFalse(viewModel.stateFlow.value.isIconLoadingDisabled)
|
||||||
|
|
||||||
|
mutableIsIconLoadingDisabledFlow.value = true
|
||||||
|
assertTrue(viewModel.stateFlow.value.isIconLoadingDisabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `RefreshPull should call vault repository sync`() {
|
||||||
|
val viewModel = createViewModel()
|
||||||
|
|
||||||
|
viewModel.trySendAction(VerificationCodeAction.RefreshPull)
|
||||||
|
|
||||||
|
verify(exactly = 1) {
|
||||||
|
vaultRepository.sync()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `PullToRefreshEnableReceive should update isPullToRefreshEnabled`() = runTest {
|
||||||
|
val viewModel = createViewModel()
|
||||||
|
|
||||||
|
viewModel.trySendAction(
|
||||||
|
VerificationCodeAction.Internal.PullToRefreshEnableReceive(
|
||||||
|
isPullToRefreshEnabled = true,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
initialState.copy(isPullToRefreshSettingEnabled = true),
|
||||||
|
viewModel.stateFlow.value,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupMockUri() {
|
||||||
|
mockkStatic(Uri::class)
|
||||||
|
val uriMock = mockk<Uri>()
|
||||||
|
every { Uri.parse(any()) } returns uriMock
|
||||||
|
every { uriMock.host } returns "www.mockuri.com"
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createViewModel(): VerificationCodeViewModel =
|
||||||
|
VerificationCodeViewModel(
|
||||||
|
clipboardManager = clipboardManager,
|
||||||
|
vaultRepository = vaultRepository,
|
||||||
|
environmentRepository = environmentRepository,
|
||||||
|
settingsRepository = settingsRepository,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Suppress("MaxLineLength")
|
||||||
|
private fun createVerificationCodeState(
|
||||||
|
viewState: VerificationCodeState.ViewState = VerificationCodeState.ViewState.Loading,
|
||||||
|
): VerificationCodeState =
|
||||||
|
VerificationCodeState(
|
||||||
|
viewState = viewState,
|
||||||
|
vaultFilterType = vaultRepository.vaultFilterType,
|
||||||
|
isIconLoadingDisabled = settingsRepository.isIconLoadingDisabled,
|
||||||
|
baseIconUrl = environmentRepository.environment.environmentUrlData.baseIconUrl,
|
||||||
|
dialogState = null,
|
||||||
|
isPullToRefreshSettingEnabled = settingsRepository.getPullToRefreshEnabledFlow().value,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val DEFAULT_STATE: VerificationCodeState = VerificationCodeState(
|
|
||||||
VerificationCodeState.ViewState.Empty,
|
|
||||||
)
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue