Display no items content when user has no items saved (#13)

This commit is contained in:
Patrick Honkonen
2024-04-03 13:10:36 -04:00
committed by GitHub
parent 6a226f21bd
commit 9aa091818d
11 changed files with 451 additions and 234 deletions
@@ -1,10 +1,7 @@
package com.x8bit.bitwarden.authenticator.data.authenticator.datasource.disk
import com.bitwarden.core.CipherRepromptType
import com.bitwarden.core.CipherType
import com.bitwarden.core.CipherView
import com.bitwarden.core.DateTime
import com.bitwarden.core.LoginView
import com.x8bit.bitwarden.authenticator.data.platform.repository.util.bufferedMutableSharedFlow
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
import javax.inject.Inject
@@ -12,9 +9,11 @@ import javax.inject.Inject
class AuthenticatorDiskSourceImpl @Inject constructor() : AuthenticatorDiskSource {
private val ciphers = STATIC_CIPHER_CACHE.toMutableList()
private val mutableCiphersFlow = bufferedMutableSharedFlow<List<CipherView>>()
override suspend fun saveCipher(cipher: CipherView) {
ciphers.add(cipher)
mutableCiphersFlow.tryEmit(ciphers)
}
override fun getCiphers(): Flow<List<CipherView>> {
@@ -23,76 +22,78 @@ class AuthenticatorDiskSourceImpl @Inject constructor() : AuthenticatorDiskSourc
override suspend fun deleteCipher(cipherId: String) {
ciphers.removeIf { it.id == cipherId }
mutableCiphersFlow.tryEmit(ciphers)
}
}
private val STATIC_CIPHER_CACHE = listOf(
CipherView(
id = "1",
organizationId = null,
folderId = null,
collectionIds = emptyList(),
key = null,
name = "TOTP test 1",
notes = null,
type = CipherType.LOGIN,
login = LoginView(
null,
null,
null,
null,
"JBSWY3DPEHPK3PXP",
null,
null
),
identity = null,
card = null,
secureNote = null,
favorite = true,
reprompt = CipherRepromptType.NONE,
organizationUseTotp = false,
edit = false,
viewPassword = true,
localData = null,
attachments = null,
fields = null,
passwordHistory = null,
creationDate = DateTime.now(),
deletedDate = null,
revisionDate = DateTime.now()
),
CipherView(
id = "2",
organizationId = null,
folderId = null,
collectionIds = emptyList(),
key = null,
name = "TOTP test 2",
notes = null,
type = CipherType.LOGIN,
login = LoginView(
null,
null,
null,
null,
"JBSWY3DPEHPK3PXP",
null,
null
),
identity = null,
card = null,
secureNote = null,
favorite = true,
reprompt = CipherRepromptType.NONE,
organizationUseTotp = false,
edit = false,
viewPassword = true,
localData = null,
attachments = null,
fields = null,
passwordHistory = null,
creationDate = DateTime.now(),
deletedDate = null,
revisionDate = DateTime.now()
)
)
private val STATIC_CIPHER_CACHE = emptyList<CipherView>()
// listOf(
// CipherView(
// id = "1",
// organizationId = null,
// folderId = null,
// collectionIds = emptyList(),
// key = null,
// name = "TOTP test 1",
// notes = null,
// type = CipherType.LOGIN,
// login = LoginView(
// null,
// null,
// null,
// null,
// "JBSWY3DPEHPK3PXP",
// null,
// null
// ),
// identity = null,
// card = null,
// secureNote = null,
// favorite = true,
// reprompt = CipherRepromptType.NONE,
// organizationUseTotp = false,
// edit = false,
// viewPassword = true,
// localData = null,
// attachments = null,
// fields = null,
// passwordHistory = null,
// creationDate = DateTime.now(),
// deletedDate = null,
// revisionDate = DateTime.now()
// ),
// CipherView(
// id = "2",
// organizationId = null,
// folderId = null,
// collectionIds = emptyList(),
// key = null,
// name = "TOTP test 2",
// notes = null,
// type = CipherType.LOGIN,
// login = LoginView(
// null,
// null,
// null,
// null,
// "JBSWY3DPEHPK3PXP",
// null,
// null
// ),
// identity = null,
// card = null,
// secureNote = null,
// favorite = true,
// reprompt = CipherRepromptType.NONE,
// organizationUseTotp = false,
// edit = false,
// viewPassword = true,
// localData = null,
// attachments = null,
// fields = null,
// passwordHistory = null,
// creationDate = DateTime.now(),
// deletedDate = null,
// revisionDate = DateTime.now()
// )
//)
@@ -36,7 +36,7 @@ import javax.inject.Inject
* A "stop timeout delay" in milliseconds used to let a shared coroutine continue to run for the
* specified period of time after it no longer has subscribers.
*/
private const val STOP_TIMEOUT_DELAY_MS: Long = 1_000L
private const val STOP_TIMEOUT_DELAY_MS: Long = 5_000L
class AuthenticatorRepositoryImpl @Inject constructor(
private val authenticatorDiskSource: AuthenticatorDiskSource,
@@ -57,7 +57,6 @@ class AuthenticatorRepositoryImpl @Inject constructor(
override val authenticatorDataFlow: StateFlow<DataState<AuthenticatorData>> =
ciphersStateFlow.map { cipherDataState ->
when (cipherDataState) {
is DataState.Error -> {
DataState.Error(
@@ -114,7 +113,7 @@ class AuthenticatorRepositoryImpl @Inject constructor(
}
.stateIn(
scope = unconfinedScope,
started = SharingStarted.Lazily,
started = SharingStarted.WhileSubscribed(STOP_TIMEOUT_DELAY_MS),
initialValue = DataState.Loading,
)
@@ -139,7 +138,7 @@ class AuthenticatorRepositoryImpl @Inject constructor(
}
.stateIn(
scope = unconfinedScope,
started = SharingStarted.WhileSubscribed(),
started = SharingStarted.WhileSubscribed(STOP_TIMEOUT_DELAY_MS),
initialValue = DataState.Loading,
)
}
@@ -177,7 +176,7 @@ class AuthenticatorRepositoryImpl @Inject constructor(
}
.stateIn(
scope = unconfinedScope,
started = SharingStarted.WhileSubscribed(),
started = SharingStarted.WhileSubscribed(STOP_TIMEOUT_DELAY_MS),
initialValue = DataState.Loading,
)
}
@@ -29,6 +29,10 @@ fun NavGraphBuilder.itemListingGraph(
onNavigateToItemScreen = { navController.navigateToItem(itemId = it) },
onNavigateToEditItemScreen = { /*navController.navigateToEditItem(itemId = it)*/ },
onNavigateToManualKeyEntry = { navController.navigateToManualCodeEntryScreen() },
onNavigateToSyncWithBitwardenScreen = {
/*navController.navigateToSyncWithBitwardenScreen()*/
},
onNavigateToImportScreen = { /*navController.navigateToImportScreen()*/ }
)
itemDestination(
onNavigateBack = { navController.popBackStack() },
@@ -14,6 +14,8 @@ fun NavGraphBuilder.itemListingDestination(
onNavigateToManualKeyEntry: () -> Unit = { },
onNavigateToItemScreen: (id: String) -> Unit = { },
onNavigateToEditItemScreen: (id: String) -> Unit = { },
onNavigateToSyncWithBitwardenScreen: () -> Unit = { },
onNavigateToImportScreen: () -> Unit = { },
) {
composableWithPushTransitions(
route = ITEM_LIST_ROUTE,
@@ -23,7 +25,9 @@ fun NavGraphBuilder.itemListingDestination(
onNavigateToQrCodeScanner = onNavigateToQrCodeScanner,
onNavigateToManualKeyEntry = onNavigateToManualKeyEntry,
onNavigateToItemScreen = onNavigateToItemScreen,
onNavigateToEditItemScreen = onNavigateToEditItemScreen
onNavigateToEditItemScreen = onNavigateToEditItemScreen,
onNavigateToSyncWithBitwardenScreen = onNavigateToSyncWithBitwardenScreen,
onNavigateToImportScreen = onNavigateToImportScreen
)
}
}
@@ -1,9 +1,13 @@
package com.x8bit.bitwarden.authenticator.ui.authenticator.feature.itemlisting
import android.widget.Toast
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
@@ -15,13 +19,17 @@ import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.testTag
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
@@ -30,6 +38,8 @@ import com.x8bit.bitwarden.authenticator.ui.authenticator.feature.itemlisting.mo
import com.x8bit.bitwarden.authenticator.ui.platform.base.util.EventsEffect
import com.x8bit.bitwarden.authenticator.ui.platform.base.util.asText
import com.x8bit.bitwarden.authenticator.ui.platform.components.appbar.BitwardenTopAppBar
import com.x8bit.bitwarden.authenticator.ui.platform.components.button.BitwardenFilledTonalButton
import com.x8bit.bitwarden.authenticator.ui.platform.components.button.BitwardenTextButton
import com.x8bit.bitwarden.authenticator.ui.platform.components.dialog.BasicDialogState
import com.x8bit.bitwarden.authenticator.ui.platform.components.dialog.BitwardenBasicDialog
import com.x8bit.bitwarden.authenticator.ui.platform.components.dialog.BitwardenLoadingDialog
@@ -38,6 +48,7 @@ import com.x8bit.bitwarden.authenticator.ui.platform.components.fab.ExpandableFa
import com.x8bit.bitwarden.authenticator.ui.platform.components.fab.ExpandableFloatingActionButton
import com.x8bit.bitwarden.authenticator.ui.platform.components.model.IconResource
import com.x8bit.bitwarden.authenticator.ui.platform.components.scaffold.BitwardenScaffold
import com.x8bit.bitwarden.authenticator.ui.platform.theme.Typography
/**
* Displays the item listing screen.
@@ -51,6 +62,8 @@ fun ItemListingScreen(
onNavigateToManualKeyEntry: () -> Unit,
onNavigateToItemScreen: (id: String) -> Unit,
onNavigateToEditItemScreen: (id: String) -> Unit,
onNavigateToSyncWithBitwardenScreen: () -> Unit,
onNavigateToImportScreen: () -> Unit,
) {
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
@@ -65,7 +78,12 @@ fun ItemListingScreen(
ItemListingEvent.NavigateToManualAddItem -> onNavigateToManualKeyEntry()
is ItemListingEvent.NavigateToItem -> onNavigateToItemScreen(event.id)
is ItemListingEvent.ShowToast -> {
Toast.makeText(context, event.message(context.resources), Toast.LENGTH_LONG)
Toast
.makeText(
context,
event.message(context.resources),
Toast.LENGTH_LONG
)
.show()
}
@@ -96,25 +114,21 @@ fun ItemListingScreen(
icon = IconResource(
iconPainter = painterResource(id = R.drawable.ic_camera),
contentDescription = stringResource(id = R.string.scan_a_qr_code),
testTag = "ScanQRCodeButton"
testTag = "ScanQRCodeButton",
),
onScanQrCodeClick = {
viewModel.trySendAction(
ItemListingAction.ScanQrCodeClick
)
viewModel.trySendAction(ItemListingAction.ScanQrCodeClick)
}
),
ItemListingExpandableFabAction.EnterSetupKey(
label = R.string.enter_a_setup_key.asText(),
icon = IconResource(
iconPainter = painterResource(id = R.drawable.ic_copy),
iconPainter = painterResource(id = R.drawable.ic_keyboard_24px),
contentDescription = stringResource(id = R.string.enter_a_setup_key),
testTag = "EnterSetupKeyButton"
testTag = "EnterSetupKeyButton",
),
onEnterSetupKeyClick = {
viewModel.trySendAction(
ItemListingAction.EnterSetupKeyClick
)
viewModel.trySendAction(ItemListingAction.EnterSetupKeyClick)
}
)
),
@@ -161,15 +175,20 @@ fun ItemListingScreen(
}
is ItemListingState.ViewState.Error -> {
Text(text = "Error! ${currentState.message}", modifier = Modifier.fillMaxSize())
Text(
text = "Error! ${currentState.message}",
modifier = Modifier.fillMaxSize(),
)
}
ItemListingState.ViewState.Loading -> {
Text(text = "Loading")
}
ItemListingState.ViewState.NoItems -> {
Text(text = "Welcome! Add a 2FA TOTP", modifier = Modifier.fillMaxSize())
ItemListingState.ViewState.NoItems,
ItemListingState.ViewState.Loading,
-> {
EmptyItemListingContent(
onAddCodeClick = onNavigateToQrCodeScanner,
onSyncWithBitwardenClick = onNavigateToSyncWithBitwardenScreen,
onImportItemsClick = onNavigateToImportScreen,
)
}
}
@@ -199,3 +218,70 @@ fun ItemListingScreen(
}
}
}
/**
* Displays the item listing screen with no existing items.
*/
@Composable
fun EmptyItemListingContent(
modifier: Modifier = Modifier,
onAddCodeClick: () -> Unit = {},
onSyncWithBitwardenClick: () -> Unit = {},
onImportItemsClick: () -> Unit = {},
) {
Column(
modifier = modifier
.fillMaxSize()
.padding(horizontal = 16.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) {
Image(
modifier = Modifier.fillMaxWidth(),
painter = painterResource(id = R.drawable.ic_empty_vault),
contentDescription = stringResource(
id = R.string.empty_item_list,
),
contentScale = ContentScale.Fit,
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = stringResource(id = R.string.you_dont_have_items_to_display),
style = Typography.titleMedium,
)
Spacer(modifier = Modifier.height(16.dp))
Text(
textAlign = TextAlign.Center,
text = stringResource(id = R.string.empty_item_list_instruction),
)
Spacer(modifier = Modifier.height(16.dp))
BitwardenFilledTonalButton(
modifier = Modifier.fillMaxWidth(),
label = stringResource(R.string.add_code),
onClick = onAddCodeClick,
)
Spacer(modifier = Modifier.height(8.dp))
BitwardenTextButton(
label = stringResource(id = R.string.sync_items_with_bitwarden),
onClick = onSyncWithBitwardenClick,
)
Spacer(modifier = Modifier.height(16.dp))
BitwardenTextButton(
label = stringResource(id = R.string.import_items),
onClick = onImportItemsClick,
)
}
}
@Composable
@Preview(showBackground = true)
fun EmptyListingContentPreview() {
EmptyItemListingContent(
modifier = Modifier.padding(horizontal = 16.dp),
)
}
@@ -7,14 +7,15 @@ import com.x8bit.bitwarden.authenticator.data.authenticator.manager.model.Verifi
import com.x8bit.bitwarden.authenticator.data.authenticator.repository.AuthenticatorRepository
import com.x8bit.bitwarden.authenticator.data.platform.repository.SettingsRepository
import com.x8bit.bitwarden.authenticator.data.platform.repository.model.DataState
import com.x8bit.bitwarden.authenticator.ui.authenticator.feature.itemlisting.model.ItemListingData
import com.x8bit.bitwarden.authenticator.ui.authenticator.feature.itemlisting.util.toViewState
import com.x8bit.bitwarden.authenticator.ui.platform.base.BaseViewModel
import com.x8bit.bitwarden.authenticator.ui.platform.base.util.Text
import com.x8bit.bitwarden.authenticator.ui.platform.base.util.asText
import com.x8bit.bitwarden.authenticator.ui.platform.base.util.concat
import com.x8bit.bitwarden.authenticator.ui.platform.components.model.IconData
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.update
import kotlinx.parcelize.Parcelize
@@ -27,77 +28,75 @@ import javax.inject.Inject
class ItemListingViewModel @Inject constructor(
authenticatorRepository: AuthenticatorRepository,
settingsRepository: SettingsRepository,
) :
BaseViewModel<ItemListingState, ItemListingEvent, ItemListingAction>(
initialState = ItemListingState(
viewState = ItemListingState.ViewState.Loading,
dialog = null
)
) {
) : BaseViewModel<ItemListingState, ItemListingEvent, ItemListingAction>(
initialState = ItemListingState(
settingsRepository.authenticatorAlertThresholdSeconds,
viewState = ItemListingState.ViewState.Loading,
dialog = null
)
) {
init {
combine(
authenticatorRepository.getAuthCodesFlow(),
settingsRepository.authenticatorAlertThresholdSecondsFlow,
) { authCodeState, alertThresholdSeconds ->
when (authCodeState) {
is DataState.Error -> {
DataState.Error(
error = authCodeState.error,
data = ItemListingData(
alertThresholdSeconds = alertThresholdSeconds,
authenticatorData = authCodeState.data,
)
)
}
is DataState.Loaded -> {
DataState.Loaded(
ItemListingData(
alertThresholdSeconds = alertThresholdSeconds,
authCodeState.data
)
)
}
settingsRepository
.authenticatorAlertThresholdSecondsFlow
.map { ItemListingAction.Internal.AlertThresholdSecondsReceive(it) }
.onEach(::sendAction)
.launchIn(viewModelScope)
DataState.Loading -> {
DataState.Loading
}
is DataState.NoNetwork -> {
DataState.NoNetwork(
ItemListingData(
alertThresholdSeconds = alertThresholdSeconds,
authenticatorData = authCodeState.data ?: emptyList()
)
)
}
is DataState.Pending -> {
DataState.Pending(
ItemListingData(
alertThresholdSeconds = alertThresholdSeconds,
authCodeState.data
)
)
}
}
}
.onEach {
sendAction(ItemListingAction.Internal.AuthenticatorDataReceive(it))
}
authenticatorRepository
.getAuthCodesFlow()
.map { ItemListingAction.Internal.AuthCodesUpdated(it) }
.onEach(::sendAction)
.launchIn(viewModelScope)
}
override fun handleAction(action: ItemListingAction) {
when (action) {
ItemListingAction.ScanQrCodeClick -> sendEvent(ItemListingEvent.NavigateToQrCodeScanner)
ItemListingAction.EnterSetupKeyClick -> sendEvent(ItemListingEvent.NavigateToManualAddItem)
ItemListingAction.BackClick -> sendEvent(ItemListingEvent.NavigateBack)
is ItemListingAction.ItemClick -> sendEvent(ItemListingEvent.NavigateToItem(action.id))
ItemListingAction.DialogDismiss -> handleDialogDismiss()
is ItemListingAction.Internal.AuthenticatorDataReceive -> handleAuthenticatorDataReceive(
action
ItemListingAction.ScanQrCodeClick -> {
sendEvent(ItemListingEvent.NavigateToQrCodeScanner)
}
ItemListingAction.EnterSetupKeyClick -> {
sendEvent(ItemListingEvent.NavigateToManualAddItem)
}
ItemListingAction.BackClick -> {
sendEvent(ItemListingEvent.NavigateBack)
}
is ItemListingAction.ItemClick -> {
sendEvent(ItemListingEvent.NavigateToItem(action.id))
}
ItemListingAction.DialogDismiss -> {
handleDialogDismiss()
}
is ItemListingAction.Internal -> {
handleInternalAction(action)
}
}
}
private fun handleInternalAction(internalAction: ItemListingAction.Internal) {
when (internalAction) {
is ItemListingAction.Internal.AuthCodesUpdated -> {
handleAuthenticatorDataReceive(internalAction)
}
is ItemListingAction.Internal.AlertThresholdSecondsReceive -> {
handleAlertThresholdSecondsReceive(internalAction)
}
}
}
private fun handleAlertThresholdSecondsReceive(
action: ItemListingAction.Internal.AlertThresholdSecondsReceive,
) {
mutableStateFlow.update {
it.copy(
alertThresholdSeconds = action.thresholdSeconds,
)
}
}
@@ -108,72 +107,50 @@ class ItemListingViewModel @Inject constructor(
}
}
private fun handleAuthenticatorDataReceive(action: ItemListingAction.Internal.AuthenticatorDataReceive) {
when (val viewState = action.itemListingDataState) {
is DataState.Error -> authenticatorErrorReceive(viewState)
is DataState.Loaded -> authenticatorDataLoadedReceive(viewState.data)
private fun handleAuthenticatorDataReceive(
action: ItemListingAction.Internal.AuthCodesUpdated,
) {
updateViewState(action.itemListingDataState)
}
private fun updateViewState(authenticatorData: DataState<List<VerificationCodeItem>>) {
when (authenticatorData) {
is DataState.Error -> authenticatorErrorReceive(authenticatorData)
is DataState.Loaded -> authenticatorDataLoadedReceive(authenticatorData)
is DataState.Loading -> authenticatorDataLoadingReceive()
is DataState.NoNetwork -> authenticatorNoNetworkReceive(viewState.data)
is DataState.Pending -> authenticatorPendingReceive()
is DataState.NoNetwork -> authenticatorNoNetworkReceive(authenticatorData)
is DataState.Pending -> authenticatorPendingReceive(authenticatorData)
}
}
private fun authenticatorErrorReceive(authenticatorData: DataState.Error<ItemListingData>) {
mutableStateFlow.update {
if (authenticatorData.data != null) {
val viewState = updateViewState(authenticatorData.data)
it.copy(
viewState = viewState,
dialog = ItemListingState.DialogState.Error(
title = R.string.an_error_has_occurred.asText(),
message = R.string.generic_error_message.asText(),
)
)
} else {
private fun authenticatorErrorReceive(authenticatorData: DataState.Error<List<VerificationCodeItem>>) {
if (authenticatorData.data != null) {
updateStateWithVerificationCodeItems(
authenticatorData = authenticatorData.data,
clearDialogState = true
)
} else {
mutableStateFlow.update {
it.copy(
viewState = ItemListingState.ViewState.Error(
R.string.generic_error_message.asText(),
)
),
dialog = null,
)
}
}
}
private fun authenticatorDataLoadedReceive(authenticatorData: ItemListingData) {
if (state.dialog == ItemListingState.DialogState.Syncing) {
sendEvent(ItemListingEvent.ShowToast(R.string.syncing_complete.asText()))
}
mutableStateFlow.update {
it.copy(
viewState = updateViewState(authenticatorData),
dialog = null
)
}
private fun authenticatorDataLoadedReceive(
authenticatorData: DataState.Loaded<List<VerificationCodeItem>>,
) {
updateStateWithVerificationCodeItems(
authenticatorData = authenticatorData.data,
clearDialogState = true
)
}
private fun authenticatorDataLoadingReceive() {
mutableStateFlow.update {
it.copy(
viewState = ItemListingState.ViewState.Loading,
dialog = ItemListingState.DialogState.Syncing
)
}
}
private fun authenticatorNoNetworkReceive(authenticatorData: ItemListingData?) {
mutableStateFlow.update {
it.copy(
viewState = updateViewState(authenticatorData),
dialog = ItemListingState.DialogState.Error(
title = R.string.internet_connection_required_title.asText(),
message = R.string.internet_connection_required_message.asText(),
)
)
}
}
private fun authenticatorPendingReceive() {
mutableStateFlow.update {
it.copy(
viewState = ItemListingState.ViewState.Loading
@@ -181,14 +158,48 @@ class ItemListingViewModel @Inject constructor(
}
}
private fun updateViewState(
itemListingData: ItemListingData?,
): ItemListingState.ViewState {
val items = itemListingData?.authenticatorData ?: return ItemListingState.ViewState.NoItems
return ItemListingState.ViewState.Content(
itemList = items.toDisplayItems(itemListingData.alertThresholdSeconds)
private fun authenticatorNoNetworkReceive(state: DataState.NoNetwork<List<VerificationCodeItem>>) {
if (state.data != null) {
updateStateWithVerificationCodeItems(
authenticatorData = state.data,
clearDialogState = true
)
} else {
mutableStateFlow.update {
it.copy(
viewState = ItemListingState.ViewState.Error(
message = R.string.internet_connection_required_title
.asText()
.concat(R.string.internet_connection_required_message.asText())
),
dialog = null,
)
}
}
}
private fun authenticatorPendingReceive(
action: DataState.Pending<List<VerificationCodeItem>>,
) {
updateStateWithVerificationCodeItems(
authenticatorData = action.data,
clearDialogState = false
)
}
private fun updateStateWithVerificationCodeItems(
authenticatorData: List<VerificationCodeItem>,
clearDialogState: Boolean,
) {
mutableStateFlow.update { currentState ->
currentState.copy(
viewState = authenticatorData.toViewState(
alertThresholdSeconds = state.alertThresholdSeconds,
),
dialog = currentState.dialog.takeUnless { clearDialogState },
)
}
}
}
/**
@@ -200,6 +211,7 @@ class ItemListingViewModel @Inject constructor(
*/
@Parcelize
data class ItemListingState(
val alertThresholdSeconds: Int,
val viewState: ViewState,
val dialog: DialogState?,
) : Parcelable {
@@ -346,14 +358,18 @@ sealed class ItemListingAction {
/**
* Indicates authenticator item listing data has been received.
*/
data class AuthenticatorDataReceive(
val itemListingDataState: DataState<ItemListingData>,
data class AuthCodesUpdated(
val itemListingDataState: DataState<List<VerificationCodeItem>>,
) : Internal()
data class AlertThresholdSecondsReceive(
val thresholdSeconds: Int,
) : Internal()
}
}
/**
* The data for the verification code item to displayed.
* The data for the verification code item to display.
*/
@Parcelize
data class VerificationCodeDisplayItem(
@@ -366,15 +382,3 @@ data class VerificationCodeDisplayItem(
val authCode: String,
val startIcon: IconData = IconData.Local(R.drawable.ic_login_item),
) : Parcelable
private fun List<VerificationCodeItem>.toDisplayItems(alertThresholdSeconds: Int) = this.map {
VerificationCodeDisplayItem(
id = it.id,
label = it.name,
authCode = it.code,
supportingLabel = it.username,
periodSeconds = it.periodSeconds,
timeLeftSeconds = it.timeLeftSeconds,
alertThresholdSeconds = alertThresholdSeconds
)
}
@@ -29,5 +29,4 @@ sealed class ItemListingExpandableFabAction(
icon,
onEnterSetupKeyClick
)
}
@@ -0,0 +1,27 @@
package com.x8bit.bitwarden.authenticator.ui.authenticator.feature.itemlisting.util
import com.x8bit.bitwarden.authenticator.data.authenticator.manager.model.VerificationCodeItem
import com.x8bit.bitwarden.authenticator.ui.authenticator.feature.itemlisting.ItemListingState
import com.x8bit.bitwarden.authenticator.ui.authenticator.feature.itemlisting.VerificationCodeDisplayItem
fun List<VerificationCodeItem>.toViewState(
alertThresholdSeconds: Int,
): ItemListingState.ViewState =
if (isEmpty()) {
ItemListingState.ViewState.NoItems
} else {
ItemListingState.ViewState.Content(
map { it.toDisplayItem(alertThresholdSeconds = alertThresholdSeconds) }
)
}
fun VerificationCodeItem.toDisplayItem(alertThresholdSeconds: Int) =
VerificationCodeDisplayItem(
id = id,
label = name,
supportingLabel = username,
timeLeftSeconds = timeLeftSeconds,
periodSeconds = periodSeconds,
alertThresholdSeconds = alertThresholdSeconds,
authCode = code,
)
@@ -0,0 +1,77 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="180dp"
android:height="161dp"
android:viewportHeight="161"
android:viewportWidth="180">
<path
android:fillColor="#E5E5EA"
android:pathData="M134,147H161V154.57C161,156.78 159.21,158.57 157,158.57H138C135.79,158.57 134,156.78 134,154.57V147Z"
android:strokeColor="#8E8E93"
android:strokeWidth="2" />
<path
android:fillColor="#E5E5EA"
android:pathData="M20,147H47V154.57C47,156.78 45.21,158.57 43,158.57H24C21.79,158.57 20,156.78 20,154.57V147Z"
android:strokeColor="#8E8E93"
android:strokeWidth="2" />
<path
android:fillColor="#F2F2F7"
android:pathData="M172.38,2H7.62C4.51,2 2,4.52 2,7.62V140.56C2,143.67 4.51,146.18 7.62,146.18H172.38C175.49,146.18 178,143.67 178,140.56V7.62C178,4.52 175.49,2 172.38,2Z"
android:strokeColor="#8E8E93"
android:strokeWidth="2" />
<path
android:fillColor="#E5E5EA"
android:pathData="M154.44,18H26.56C23.49,18 21,20.49 21,23.56V123.64C21,126.71 23.49,129.2 26.56,129.2H154.44C157.51,129.2 160,126.71 160,123.64V23.56C160,20.49 157.51,18 154.44,18Z"
android:strokeColor="#8E8E93"
android:strokeWidth="2" />
<path
android:fillColor="#F2F2F7"
android:pathData="M154.76,129.8L49.89,135.92C46.69,136.11 44,133.57 44,130.38V17.55C44,14.36 46.69,11.82 49.89,12.01L154.76,18.13C157.7,18.3 160,20.73 160,23.67V124.26C160,127.2 157.7,129.63 154.76,129.8Z"
android:strokeColor="#8E8E93"
android:strokeWidth="2" />
<path
android:fillColor="#F2F2F7"
android:pathData="M164.85,40.87C164.85,38.55 162.98,36.68 160.66,36.68C158.34,36.68 156.46,38.55 156.46,40.87V57.63C156.46,59.94 158.34,61.82 160.66,61.82C162.98,61.82 164.85,59.94 164.85,57.63V40.87Z"
android:strokeColor="#8E8E93"
android:strokeWidth="1" />
<path
android:fillColor="#F2F2F7"
android:pathData="M164.85,82.77C164.85,80.45 162.98,78.58 160.66,78.58C158.34,78.58 156.46,80.45 156.46,82.77V99.53C156.46,101.84 158.34,103.71 160.66,103.71C162.98,103.71 164.85,101.84 164.85,99.53V82.77Z"
android:strokeColor="#8E8E93"
android:strokeWidth="1" />
<path
android:fillColor="#00000000"
android:pathData="M62,45C63.66,45 65,46.26 65,47.81V86.44C65,87.99 63.66,89.25 62,89.25"
android:strokeColor="#8E8E93"
android:strokeLineCap="round"
android:strokeWidth="2" />
<path
android:fillColor="#8E8E93"
android:pathData="M109,86.29C103.24,86.29 98.56,80.74 98.56,73.92C98.56,67.09 103.24,61.55 109,61.55C114.76,61.55 119.42,67.1 119.42,73.92C119.42,80.73 114.75,86.29 109,86.29ZM109,59.83C102.45,59.83 97.12,66.15 97.12,73.92C97.12,81.69 102.45,88.01 109,88.01C115.55,88.01 120.87,81.7 120.87,73.92C120.87,66.14 115.54,59.83 109,59.83Z" />
<path
android:fillColor="#8E8E93"
android:pathData="M107.55,53.53C103.88,53.9 100.54,55.64 97.88,58.31L98.85,59.46C99.13,59.79 99.27,60.23 99.27,60.67C99.27,61.1 99.13,61.54 98.85,61.87C98.58,62.22 98.2,62.38 97.84,62.38C97.47,62.38 97.11,62.22 96.82,61.87L95.86,60.72C93.6,63.87 92.14,67.84 91.84,72.21H93.21C94,72.21 94.65,72.97 94.65,73.91C94.65,74.84 94,75.62 93.21,75.62H91.84C92.14,79.98 93.61,83.96 95.86,87.1L96.82,85.95C97.11,85.61 97.47,85.45 97.84,85.45C98.2,85.45 98.58,85.61 98.85,85.95C99.13,86.28 99.27,86.73 99.27,87.16C99.27,87.59 99.13,88.04 98.85,88.37L97.88,89.52C100.54,92.2 103.89,93.94 107.57,94.29V92.65C107.57,91.71 108.22,90.94 109.01,90.94C101.11,90.94 94.66,83.3 94.66,73.91C94.66,64.52 101.11,56.88 109.01,56.88C108.22,56.88 107.57,56.11 107.57,55.17V53.53M110.45,53.53V55.17C110.45,56.12 109.8,56.88 109.01,56.88C116.92,56.88 123.35,64.52 123.35,73.91C123.35,83.3 116.91,90.94 109.01,90.94C109.8,90.94 110.45,91.72 110.45,92.65V94.29C114.12,93.93 117.47,92.2 120.13,89.52L119.17,88.36C118.88,88.03 118.74,87.58 118.74,87.15C118.74,86.72 118.88,86.27 119.17,85.94C119.43,85.6 119.81,85.44 120.18,85.44C120.54,85.44 120.92,85.6 121.2,85.94L122.17,87.09C124.41,83.93 125.88,79.97 126.19,75.6H124.81C124.01,75.6 123.36,74.84 123.36,73.9C123.36,72.95 124.01,72.2 124.81,72.2H126.19C125.88,67.83 124.41,63.87 122.17,60.71L121.2,61.86C120.92,62.2 120.54,62.36 120.18,62.36C119.81,62.36 119.43,62.2 119.17,61.86C118.88,61.53 118.74,61.09 118.74,60.65C118.74,60.22 118.88,59.78 119.17,59.45L120.13,58.3C117.46,55.62 114.12,53.89 110.45,53.52" />
<path
android:fillColor="#8E8E93"
android:pathData="M109,46C108.21,46 107.55,46.76 107.55,47.71V55.17C107.55,56.12 108.21,56.88 109,56.88C109.79,56.88 110.43,56.11 110.43,55.17V47.71C110.43,46.76 109.8,46 109,46Z" />
<path
android:fillColor="#8E8E93"
android:pathData="M93.38,53.67C93.01,53.67 92.65,53.84 92.36,54.18C91.8,54.84 91.8,55.93 92.36,56.59L95.85,60.72L96.81,61.87C97.09,62.22 97.46,62.37 97.82,62.37C98.19,62.37 98.56,62.22 98.84,61.87C99.12,61.54 99.26,61.1 99.26,60.67C99.26,60.23 99.12,59.79 98.84,59.46L97.87,58.31L94.39,54.18C94.11,53.84 93.73,53.67 93.36,53.67" />
<path
android:fillColor="#8E8E93"
android:pathData="M93.21,72.21H86.92C86.13,72.21 85.47,72.97 85.47,73.91C85.47,74.84 86.13,75.62 86.92,75.62H93.21C94,75.62 94.65,74.85 94.65,73.91C94.65,72.96 94,72.21 93.21,72.21Z" />
<path
android:fillColor="#8E8E93"
android:pathData="M97.84,85.46C97.47,85.46 97.11,85.62 96.82,85.96L95.86,87.11L92.38,91.25C91.81,91.91 91.81,92.99 92.38,93.65C92.66,93.99 93.02,94.16 93.39,94.16C93.75,94.16 94.13,93.99 94.41,93.65L97.88,89.52L98.85,88.37C99.13,88.04 99.27,87.59 99.27,87.16C99.27,86.73 99.13,86.28 98.85,85.95C98.58,85.61 98.2,85.45 97.84,85.45" />
<path
android:fillColor="#8E8E93"
android:pathData="M109,90.95C108.21,90.95 107.55,91.73 107.55,92.66V100.13C107.55,101.07 108.21,101.84 109,101.84C109.79,101.84 110.43,101.07 110.43,100.13V92.66C110.43,91.72 109.79,90.95 109,90.95Z" />
<path
android:fillColor="#8E8E93"
android:pathData="M120.17,85.46C119.8,85.46 119.42,85.62 119.15,85.96C118.87,86.29 118.73,86.74 118.73,87.17C118.73,87.6 118.87,88.05 119.15,88.38L120.12,89.54L123.59,93.67C123.87,94 124.25,94.17 124.61,94.17C124.98,94.17 125.35,94 125.64,93.67C126.2,93.01 126.2,91.92 125.64,91.26L122.15,87.13L121.19,85.97C120.91,85.63 120.53,85.47 120.17,85.47" />
<path
android:fillColor="#8E8E93"
android:pathData="M131.08,72.21H124.8C124,72.21 123.35,72.97 123.35,73.91C123.35,74.84 124,75.62 124.8,75.62H131.08C131.87,75.62 132.53,74.85 132.53,73.91C132.53,72.96 131.87,72.21 131.08,72.21Z" />
<path
android:fillColor="#8E8E93"
android:pathData="M124.61,53.67C124.25,53.67 123.87,53.84 123.59,54.18L120.12,58.31L119.15,59.46C118.87,59.79 118.73,60.23 118.73,60.67C118.73,61.1 118.87,61.54 119.15,61.87C119.42,62.22 119.8,62.37 120.17,62.37C120.53,62.37 120.91,62.22 121.19,61.87L122.15,60.72L125.64,56.59C126.2,55.93 126.2,54.84 125.64,54.18C125.35,53.84 124.98,53.67 124.61,53.67Z" />
</vector>
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportHeight="24"
android:viewportWidth="24">
<path
android:fillColor="#000000"
android:fillType="evenOdd"
android:pathData="M20,5H4C2.9,5 2.01,5.9 2.01,7L2,17C2,18.1 2.9,19 4,19H20C21.1,19 22,18.1 22,17V7C22,5.9 21.1,5 20,5ZM20,7V17H4V7H20ZM13,8H11V10H13V8ZM11,11H13V13H11V11ZM10,8H8V10H10V8ZM8,11H10V13H8V11ZM7,11H5V13H7V11ZM5,8H7V10H5V8ZM16,14H8V16H16V14ZM14,11H16V13H14V11ZM16,8H14V10H16V8ZM17,11H19V13H17V11ZM19,8H17V10H19V8Z" />
</vector>
+6
View File
@@ -32,4 +32,10 @@
<string name="no_thanks">No thanks</string>
<string name="settings">Settings</string>
<string name="enable_camer_permission_to_use_the_scanner">Enable camera permission to use the scanner</string>
<string name="empty_item_list">Empty Item Listing</string>
<string name="you_dont_have_items_to_display">You don\'t have any items to display.</string>
<string name="empty_item_list_instruction">Add a new code, sync an existing account, or import codes to secure your accounts.</string>
<string name="add_code">Add code</string>
<string name="sync_items_with_bitwarden">Sync items from Bitwarden</string>
<string name="import_items">Import items</string>
</resources>