mirror of
https://github.com/bitwarden/android.git
synced 2026-06-09 08:09:16 -05:00
BIT-279, BIT-1201: Storage, retrieval, and clearing implementation for password history (#416)
This commit is contained in:
committed by
Álison Fernandes
parent
39e285fff8
commit
27140bf02c
@@ -25,6 +25,7 @@ import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import java.time.Instant
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
@@ -67,7 +68,11 @@ class GeneratorRepositoryImpl(
|
||||
}
|
||||
.onEach { encryptedPasswordHistoryListResult ->
|
||||
mutablePasswordHistoryStateFlow.value = encryptedPasswordHistoryListResult.fold(
|
||||
onSuccess = { LocalDataState.Loaded(it) },
|
||||
onSuccess = {
|
||||
LocalDataState.Loaded(
|
||||
it.sortedByDescending { history -> history.lastUsedDate },
|
||||
)
|
||||
},
|
||||
onFailure = { LocalDataState.Error(it) },
|
||||
)
|
||||
}
|
||||
@@ -78,7 +83,14 @@ class GeneratorRepositoryImpl(
|
||||
generatorSdkSource
|
||||
.generatePassword(passwordGeneratorRequest)
|
||||
.fold(
|
||||
onSuccess = { GeneratedPasswordResult.Success(it) },
|
||||
onSuccess = { generatedPassword ->
|
||||
val passwordHistoryView = PasswordHistoryView(
|
||||
password = generatedPassword,
|
||||
lastUsedDate = Instant.now(),
|
||||
)
|
||||
storePasswordHistory(passwordHistoryView)
|
||||
GeneratedPasswordResult.Success(generatedPassword)
|
||||
},
|
||||
onFailure = { GeneratedPasswordResult.InvalidRequest },
|
||||
)
|
||||
|
||||
@@ -88,7 +100,14 @@ class GeneratorRepositoryImpl(
|
||||
generatorSdkSource
|
||||
.generatePassphrase(passphraseGeneratorRequest)
|
||||
.fold(
|
||||
onSuccess = { GeneratedPassphraseResult.Success(it) },
|
||||
onSuccess = { generatedPassphrase ->
|
||||
val passwordHistoryView = PasswordHistoryView(
|
||||
password = generatedPassphrase,
|
||||
lastUsedDate = Instant.now(),
|
||||
)
|
||||
storePasswordHistory(passwordHistoryView)
|
||||
GeneratedPassphraseResult.Success(generatedPassphrase)
|
||||
},
|
||||
onFailure = { GeneratedPassphraseResult.InvalidRequest },
|
||||
)
|
||||
|
||||
|
||||
@@ -28,11 +28,14 @@ import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.ui.platform.ClipboardManager
|
||||
import androidx.compose.ui.platform.LocalClipboardManager
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.toAnnotatedString
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenOverflowActionItem
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenScaffold
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenTopAppBar
|
||||
@@ -48,6 +51,7 @@ import kotlinx.collections.immutable.persistentListOf
|
||||
fun PasswordHistoryScreen(
|
||||
onNavigateBack: () -> Unit,
|
||||
viewModel: PasswordHistoryViewModel = hiltViewModel(),
|
||||
clipboardManager: ClipboardManager = LocalClipboardManager.current,
|
||||
) {
|
||||
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
|
||||
val context = LocalContext.current
|
||||
@@ -56,6 +60,11 @@ fun PasswordHistoryScreen(
|
||||
EventsEffect(viewModel = viewModel) { event ->
|
||||
when (event) {
|
||||
PasswordHistoryEvent.NavigateBack -> onNavigateBack.invoke()
|
||||
|
||||
is PasswordHistoryEvent.CopyTextToClipboard -> {
|
||||
clipboardManager.setText(event.text.toAnnotatedString())
|
||||
}
|
||||
|
||||
is PasswordHistoryEvent.ShowToast -> {
|
||||
Toast.makeText(context, event.message, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
@@ -189,7 +198,7 @@ private fun PasswordHistoryError(
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Text(
|
||||
text = state.message,
|
||||
text = state.message.invoke(),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
Spacer(modifier = Modifier.navigationBarsPadding())
|
||||
|
||||
@@ -1,9 +1,22 @@
|
||||
package com.x8bit.bitwarden.ui.tools.feature.generator.passwordhistory
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.bitwarden.core.PasswordHistoryView
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.LocalDataState
|
||||
import com.x8bit.bitwarden.data.tools.generator.repository.GeneratorRepository
|
||||
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.tools.feature.generator.passwordhistory.PasswordHistoryState.GeneratedPassword
|
||||
import com.x8bit.bitwarden.ui.tools.feature.generator.util.toFormattedPattern
|
||||
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.coroutines.launch
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import javax.inject.Inject
|
||||
|
||||
@@ -12,16 +25,61 @@ import javax.inject.Inject
|
||||
*/
|
||||
@HiltViewModel
|
||||
@Suppress("TooManyFunctions")
|
||||
class PasswordHistoryViewModel @Inject constructor() :
|
||||
BaseViewModel<PasswordHistoryState, PasswordHistoryEvent, PasswordHistoryAction>(
|
||||
initialState = PasswordHistoryState(PasswordHistoryState.ViewState.Loading),
|
||||
) {
|
||||
class PasswordHistoryViewModel @Inject constructor(
|
||||
private val generatorRepository: GeneratorRepository,
|
||||
) : BaseViewModel<PasswordHistoryState, PasswordHistoryEvent, PasswordHistoryAction>(
|
||||
initialState = PasswordHistoryState(PasswordHistoryState.ViewState.Loading),
|
||||
) {
|
||||
|
||||
init {
|
||||
generatorRepository
|
||||
.passwordHistoryStateFlow
|
||||
.map { PasswordHistoryAction.Internal.UpdatePasswordHistoryReceive(it) }
|
||||
.onEach(::sendAction)
|
||||
.launchIn(viewModelScope)
|
||||
}
|
||||
|
||||
override fun handleAction(action: PasswordHistoryAction) {
|
||||
when (action) {
|
||||
PasswordHistoryAction.CloseClick -> handleCloseClick()
|
||||
is PasswordHistoryAction.PasswordCopyClick -> handleCopyClick(action.password)
|
||||
PasswordHistoryAction.PasswordClearClick -> handlePasswordHistoryClearClick()
|
||||
is PasswordHistoryAction.Internal.UpdatePasswordHistoryReceive -> {
|
||||
handleUpdatePasswordHistoryReceive(action)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleUpdatePasswordHistoryReceive(
|
||||
action: PasswordHistoryAction.Internal.UpdatePasswordHistoryReceive,
|
||||
) {
|
||||
val newState = when (val state = action.state) {
|
||||
is LocalDataState.Loading -> PasswordHistoryState.ViewState.Loading
|
||||
|
||||
is LocalDataState.Error -> {
|
||||
PasswordHistoryState.ViewState.Error(R.string.an_error_has_occurred.asText())
|
||||
}
|
||||
|
||||
is LocalDataState.Loaded -> {
|
||||
val passwords = state.data.map { passwordHistoryView ->
|
||||
GeneratedPassword(
|
||||
password = passwordHistoryView.password,
|
||||
date = passwordHistoryView.lastUsedDate.toFormattedPattern(
|
||||
pattern = "MM/dd/yy h:mm a",
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
if (passwords.isEmpty()) {
|
||||
PasswordHistoryState.ViewState.Empty
|
||||
} else {
|
||||
PasswordHistoryState.ViewState.Content(passwords)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mutableStateFlow.update {
|
||||
it.copy(viewState = newState)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,19 +90,13 @@ class PasswordHistoryViewModel @Inject constructor() :
|
||||
}
|
||||
|
||||
private fun handlePasswordHistoryClearClick() {
|
||||
sendEvent(
|
||||
event = PasswordHistoryEvent.ShowToast(
|
||||
message = "Not yet implemented.",
|
||||
),
|
||||
)
|
||||
viewModelScope.launch {
|
||||
generatorRepository.clearPasswordHistory()
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleCopyClick(password: GeneratedPassword) {
|
||||
sendEvent(
|
||||
event = PasswordHistoryEvent.ShowToast(
|
||||
message = "Not yet implemented.",
|
||||
),
|
||||
)
|
||||
sendEvent(PasswordHistoryEvent.CopyTextToClipboard(password.password))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,7 +128,7 @@ data class PasswordHistoryState(
|
||||
* @property message The error message to be displayed.
|
||||
*/
|
||||
@Parcelize
|
||||
data class Error(val message: String) : ViewState()
|
||||
data class Error(val message: Text) : ViewState()
|
||||
|
||||
/**
|
||||
* Empty state for the password history screen.
|
||||
@@ -122,6 +174,11 @@ sealed class PasswordHistoryEvent {
|
||||
* Event to navigate back to the previous screen.
|
||||
*/
|
||||
data object NavigateBack : PasswordHistoryEvent()
|
||||
|
||||
/**
|
||||
* Copies text to the clipboard.
|
||||
*/
|
||||
data class CopyTextToClipboard(val text: String) : PasswordHistoryEvent()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -145,4 +202,17 @@ sealed class PasswordHistoryAction {
|
||||
* Action when the close button is clicked.
|
||||
*/
|
||||
data object CloseClick : PasswordHistoryAction()
|
||||
|
||||
/**
|
||||
* Models actions that the [PasswordHistoryViewModel] itself might send.
|
||||
*/
|
||||
sealed class Internal : PasswordHistoryAction() {
|
||||
|
||||
/**
|
||||
* Indicates a password history update is received.
|
||||
*/
|
||||
data class UpdatePasswordHistoryReceive(
|
||||
val state: LocalDataState<List<PasswordHistoryView>>,
|
||||
) : Internal()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.x8bit.bitwarden.ui.tools.feature.generator.util
|
||||
|
||||
import java.time.Instant
|
||||
import java.time.ZoneId
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.util.TimeZone
|
||||
|
||||
/**
|
||||
* Converts the [Instant] to a formatted string based on the provided pattern and time zone.
|
||||
*/
|
||||
fun Instant.toFormattedPattern(
|
||||
pattern: String,
|
||||
zone: ZoneId = TimeZone.getDefault().toZoneId(),
|
||||
): String {
|
||||
val formatter = DateTimeFormatter.ofPattern(pattern).withZone(zone)
|
||||
return formatter.format(this)
|
||||
}
|
||||
Reference in New Issue
Block a user