diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/authenticator/data/authenticator/datasource/disk/AuthenticatorDiskSourceImpl.kt b/app/src/main/kotlin/com/x8bit/bitwarden/authenticator/data/authenticator/datasource/disk/AuthenticatorDiskSourceImpl.kt index 49befdcaa..491809025 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/authenticator/data/authenticator/datasource/disk/AuthenticatorDiskSourceImpl.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/authenticator/data/authenticator/datasource/disk/AuthenticatorDiskSourceImpl.kt @@ -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>() override suspend fun saveCipher(cipher: CipherView) { ciphers.add(cipher) + mutableCiphersFlow.tryEmit(ciphers) } override fun getCiphers(): Flow> { @@ -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() +// 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() +// ) +//) diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/authenticator/data/authenticator/repository/AuthenticatorRepositoryImpl.kt b/app/src/main/kotlin/com/x8bit/bitwarden/authenticator/data/authenticator/repository/AuthenticatorRepositoryImpl.kt index d4e43ee7b..83c06534c 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/authenticator/data/authenticator/repository/AuthenticatorRepositoryImpl.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/authenticator/data/authenticator/repository/AuthenticatorRepositoryImpl.kt @@ -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> = 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, ) } diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/authenticator/ui/authenticator/feature/itemlisting/ItemListingGraphNavigation.kt b/app/src/main/kotlin/com/x8bit/bitwarden/authenticator/ui/authenticator/feature/itemlisting/ItemListingGraphNavigation.kt index 4f6f13db1..a5dc3f43e 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/authenticator/ui/authenticator/feature/itemlisting/ItemListingGraphNavigation.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/authenticator/ui/authenticator/feature/itemlisting/ItemListingGraphNavigation.kt @@ -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() }, diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/authenticator/ui/authenticator/feature/itemlisting/ItemListingNavigation.kt b/app/src/main/kotlin/com/x8bit/bitwarden/authenticator/ui/authenticator/feature/itemlisting/ItemListingNavigation.kt index eb29eb666..9fb55e305 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/authenticator/ui/authenticator/feature/itemlisting/ItemListingNavigation.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/authenticator/ui/authenticator/feature/itemlisting/ItemListingNavigation.kt @@ -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 ) } } diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/authenticator/ui/authenticator/feature/itemlisting/ItemListingScreen.kt b/app/src/main/kotlin/com/x8bit/bitwarden/authenticator/ui/authenticator/feature/itemlisting/ItemListingScreen.kt index 0318b1894..f4bbb58b3 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/authenticator/ui/authenticator/feature/itemlisting/ItemListingScreen.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/authenticator/ui/authenticator/feature/itemlisting/ItemListingScreen.kt @@ -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()) - } - - ItemListingState.ViewState.Loading -> { - Text(text = "Loading") + Text( + text = "Error! ${currentState.message}", + modifier = Modifier.fillMaxSize(), + ) } - 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), + ) +} diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/authenticator/ui/authenticator/feature/itemlisting/ItemListingViewModel.kt b/app/src/main/kotlin/com/x8bit/bitwarden/authenticator/ui/authenticator/feature/itemlisting/ItemListingViewModel.kt index 635438908..48cdcad48 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/authenticator/ui/authenticator/feature/itemlisting/ItemListingViewModel.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/authenticator/ui/authenticator/feature/itemlisting/ItemListingViewModel.kt @@ -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( - initialState = ItemListingState( - viewState = ItemListingState.ViewState.Loading, - dialog = null - ) - ) { +) : BaseViewModel( + 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 - ) - ) - } - - 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)) - } + + settingsRepository + .authenticatorAlertThresholdSecondsFlow + .map { ItemListingAction.Internal.AlertThresholdSecondsReceive(it) } + .onEach(::sendAction) + .launchIn(viewModelScope) + + 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,86 +107,98 @@ 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>) { + 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) { - 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>) { + 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>, + ) { + updateStateWithVerificationCodeItems( + authenticatorData = authenticatorData.data, + clearDialogState = true + ) } private fun authenticatorDataLoadingReceive() { mutableStateFlow.update { it.copy( - viewState = ItemListingState.ViewState.Loading, - dialog = ItemListingState.DialogState.Syncing + viewState = ItemListingState.ViewState.Loading ) } } - 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 authenticatorNoNetworkReceive(state: DataState.NoNetwork>) { + 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() { - mutableStateFlow.update { - it.copy( - viewState = ItemListingState.ViewState.Loading - ) - } + private fun authenticatorPendingReceive( + action: DataState.Pending>, + ) { + updateStateWithVerificationCodeItems( + authenticatorData = action.data, + clearDialogState = false + ) } - 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 updateStateWithVerificationCodeItems( + authenticatorData: List, + 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, + data class AuthCodesUpdated( + val itemListingDataState: DataState>, + ) : 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.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 - ) -} diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/authenticator/ui/authenticator/feature/itemlisting/model/ItemListingExpandableFabAction.kt b/app/src/main/kotlin/com/x8bit/bitwarden/authenticator/ui/authenticator/feature/itemlisting/model/ItemListingExpandableFabAction.kt index 6f58764d5..64f16031a 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/authenticator/ui/authenticator/feature/itemlisting/model/ItemListingExpandableFabAction.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/authenticator/ui/authenticator/feature/itemlisting/model/ItemListingExpandableFabAction.kt @@ -29,5 +29,4 @@ sealed class ItemListingExpandableFabAction( icon, onEnterSetupKeyClick ) - } diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/authenticator/ui/authenticator/feature/itemlisting/util/VerificationCodeItemExtensions.kt b/app/src/main/kotlin/com/x8bit/bitwarden/authenticator/ui/authenticator/feature/itemlisting/util/VerificationCodeItemExtensions.kt new file mode 100644 index 000000000..fbe9e5448 --- /dev/null +++ b/app/src/main/kotlin/com/x8bit/bitwarden/authenticator/ui/authenticator/feature/itemlisting/util/VerificationCodeItemExtensions.kt @@ -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.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, + ) diff --git a/app/src/main/res/drawable/ic_empty_vault.xml b/app/src/main/res/drawable/ic_empty_vault.xml new file mode 100644 index 000000000..b14c68cd7 --- /dev/null +++ b/app/src/main/res/drawable/ic_empty_vault.xml @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_keyboard_24px.xml b/app/src/main/res/drawable/ic_keyboard_24px.xml new file mode 100644 index 000000000..cfdcd0f52 --- /dev/null +++ b/app/src/main/res/drawable/ic_keyboard_24px.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 92f7e2b0e..945a6581d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -32,4 +32,10 @@ No thanks Settings Enable camera permission to use the scanner + Empty Item Listing + You don\'t have any items to display. + Add a new code, sync an existing account, or import codes to secure your accounts. + Add code + Sync items from Bitwarden + Import items