mirror of
https://github.com/bitwarden/android.git
synced 2026-03-11 20:54:58 -05:00
Change color of countdown indicator when period nears expiration (#5)
This commit is contained in:
@@ -18,6 +18,24 @@ interface SettingsDiskSource {
|
||||
*/
|
||||
val appThemeFlow: Flow<AppTheme>
|
||||
|
||||
/**
|
||||
* Stores the threshold at which users are alerted that an items validity period is nearing
|
||||
* expiration.
|
||||
*/
|
||||
fun storeAlertThresholdSeconds(thresholdSeconds: Int)
|
||||
|
||||
/**
|
||||
* Gets the threshold at which users are alerted that an items validity period is nearing
|
||||
* expiration.
|
||||
*/
|
||||
fun getAlertThresholdSeconds(): Int
|
||||
|
||||
/**
|
||||
* Emits updates that track the threshold at which users are alerted that an items validity
|
||||
* period is nearing expiration.
|
||||
*/
|
||||
fun getAlertThresholdSecondsFlow(): Flow<Int>
|
||||
|
||||
/**
|
||||
* Clears all the settings data for the given user.
|
||||
*/
|
||||
|
||||
@@ -11,6 +11,7 @@ import kotlinx.coroutines.flow.onSubscription
|
||||
private const val APP_THEME_KEY = "$BASE_KEY:theme"
|
||||
private const val SCREEN_CAPTURE_ALLOW_KEY = "$BASE_KEY:screenCaptureAllowed"
|
||||
private const val ACCOUNT_BIOMETRIC_INTEGRITY_VALID_KEY = "$BASE_KEY:accountBiometricIntegrityValid"
|
||||
private const val ALERT_THRESHOLD_SECONDS_KEY = "$BASE_KEY:alertThresholdSeconds"
|
||||
|
||||
/**
|
||||
* Primary implementation of [SettingsDiskSource].
|
||||
@@ -25,6 +26,9 @@ class SettingsDiskSourceImpl(
|
||||
private val mutableScreenCaptureAllowedFlowMap =
|
||||
mutableMapOf<String, MutableSharedFlow<Boolean?>>()
|
||||
|
||||
private val mutableAlertThresholdSecondsFlow =
|
||||
bufferedMutableSharedFlow<Int>()
|
||||
|
||||
override var appTheme: AppTheme
|
||||
get() = getString(key = APP_THEME_KEY)
|
||||
?.let { storedValue ->
|
||||
@@ -43,6 +47,21 @@ class SettingsDiskSourceImpl(
|
||||
get() = mutableAppThemeFlow
|
||||
.onSubscription { emit(appTheme) }
|
||||
|
||||
override fun storeAlertThresholdSeconds(thresholdSeconds: Int) {
|
||||
putInt(
|
||||
ALERT_THRESHOLD_SECONDS_KEY,
|
||||
thresholdSeconds
|
||||
)
|
||||
mutableAlertThresholdSecondsFlow.tryEmit(thresholdSeconds)
|
||||
}
|
||||
|
||||
override fun getAlertThresholdSeconds(): Int {
|
||||
return getInt(ALERT_THRESHOLD_SECONDS_KEY, default = 7) ?: 7
|
||||
}
|
||||
|
||||
override fun getAlertThresholdSecondsFlow(): Flow<Int> = mutableAlertThresholdSecondsFlow
|
||||
.onSubscription { emit(getAlertThresholdSeconds()) }
|
||||
|
||||
override fun clearData(userId: String) {
|
||||
storeScreenCaptureAllowed(userId = userId, isScreenCaptureAllowed = null)
|
||||
removeWithPrefix(prefix = "${ACCOUNT_BIOMETRIC_INTEGRITY_VALID_KEY}_$userId")
|
||||
|
||||
@@ -17,4 +17,14 @@ interface SettingsRepository {
|
||||
* Tracks changes to the [AppTheme].
|
||||
*/
|
||||
val appThemeStateFlow: StateFlow<AppTheme>
|
||||
|
||||
/**
|
||||
* The currently stored expiration alert threshold.
|
||||
*/
|
||||
var authenticatorAlertThresholdSeconds : Int
|
||||
|
||||
/**
|
||||
* Tracks changes to the expiration alert threshold.
|
||||
*/
|
||||
val authenticatorAlertThresholdSecondsFlow: StateFlow<Int>
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import com.x8bit.bitwarden.authenticator.ui.platform.feature.settings.appearance
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
|
||||
/**
|
||||
@@ -20,6 +21,8 @@ class SettingsRepositoryImpl(
|
||||
|
||||
override var appTheme: AppTheme by settingsDiskSource::appTheme
|
||||
|
||||
override var authenticatorAlertThresholdSeconds = settingsDiskSource.getAlertThresholdSeconds()
|
||||
|
||||
override val appThemeStateFlow: StateFlow<AppTheme>
|
||||
get() = settingsDiskSource
|
||||
.appThemeFlow
|
||||
@@ -28,4 +31,15 @@ class SettingsRepositoryImpl(
|
||||
started = SharingStarted.Eagerly,
|
||||
initialValue = settingsDiskSource.appTheme,
|
||||
)
|
||||
|
||||
override val authenticatorAlertThresholdSecondsFlow: StateFlow<Int>
|
||||
get() = settingsDiskSource
|
||||
.getAlertThresholdSecondsFlow()
|
||||
.map { it }
|
||||
.stateIn(
|
||||
scope = unconfinedScope,
|
||||
started = SharingStarted.Eagerly,
|
||||
initialValue = settingsDiskSource.getAlertThresholdSeconds(),
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.x8bit.bitwarden.authenticator.R
|
||||
import com.x8bit.bitwarden.authenticator.ui.platform.base.util.EventsEffect
|
||||
import com.x8bit.bitwarden.authenticator.ui.platform.components.appbar.BitwardenTopAppBar
|
||||
import com.x8bit.bitwarden.authenticator.ui.platform.components.field.BitwardenTextField
|
||||
import com.x8bit.bitwarden.authenticator.ui.platform.components.field.BitwardenTextFieldWithActions
|
||||
import com.x8bit.bitwarden.authenticator.ui.platform.components.icon.BitwardenIconButtonWithResource
|
||||
import com.x8bit.bitwarden.authenticator.ui.platform.components.indicator.BitwardenCircularCountdownIndicator
|
||||
@@ -123,11 +124,22 @@ fun ItemContent(
|
||||
) {
|
||||
LazyColumn(modifier = modifier) {
|
||||
|
||||
item {
|
||||
BitwardenTextField(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
label = stringResource(id = R.string.name),
|
||||
value = viewState.itemData.name,
|
||||
onValueChange = {},
|
||||
readOnly = true,
|
||||
singleLine = true,
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
BitwardenTextFieldWithActions(
|
||||
label = stringResource(id = R.string.verification_code_totp),
|
||||
value = viewState.totpCodeItemData.verificationCode
|
||||
value = viewState.itemData.totpCodeItemData?.verificationCode.orEmpty()
|
||||
.chunked(AUTH_CODE_SPACING_INTERVAL)
|
||||
.joinToString(" "),
|
||||
onValueChange = { },
|
||||
@@ -135,8 +147,9 @@ fun ItemContent(
|
||||
singleLine = true,
|
||||
actions = {
|
||||
BitwardenCircularCountdownIndicator(
|
||||
timeLeftSeconds = viewState.totpCodeItemData.timeLeftSeconds,
|
||||
periodSeconds = viewState.totpCodeItemData.periodSeconds,
|
||||
timeLeftSeconds = viewState.itemData.totpCodeItemData?.timeLeftSeconds ?: 0,
|
||||
periodSeconds = viewState.itemData.totpCodeItemData?.periodSeconds ?: 0,
|
||||
alertThresholdSeconds = viewState.itemData.alertThresholdSeconds
|
||||
)
|
||||
BitwardenIconButtonWithResource(
|
||||
iconRes = IconResource(
|
||||
|
||||
@@ -6,6 +6,7 @@ import androidx.lifecycle.viewModelScope
|
||||
import com.bitwarden.core.CipherView
|
||||
import com.x8bit.bitwarden.authenticator.data.authenticator.repository.AuthenticatorRepository
|
||||
import com.x8bit.bitwarden.authenticator.data.authenticator.repository.model.DeleteItemResult
|
||||
import com.x8bit.bitwarden.authenticator.data.platform.repository.SettingsRepository
|
||||
import com.x8bit.bitwarden.authenticator.data.platform.repository.model.DataState
|
||||
import com.x8bit.bitwarden.authenticator.data.platform.repository.util.combineDataStates
|
||||
import com.x8bit.bitwarden.authenticator.ui.authenticator.feature.item.model.ItemData
|
||||
@@ -28,6 +29,7 @@ private const val KEY_STATE = "state"
|
||||
@HiltViewModel
|
||||
class ItemViewModel @Inject constructor(
|
||||
authenticatorRepository: AuthenticatorRepository,
|
||||
settingsRepository: SettingsRepository,
|
||||
savedStateHandle: SavedStateHandle,
|
||||
) : BaseViewModel<ItemState, ItemEvent, ItemAction>(
|
||||
initialState = savedStateHandle[KEY_STATE] ?: ItemState(
|
||||
@@ -40,21 +42,28 @@ class ItemViewModel @Inject constructor(
|
||||
init {
|
||||
combine(
|
||||
authenticatorRepository.getItemStateFlow(state.itemId),
|
||||
authenticatorRepository.getAuthCodeFlow(state.itemId)
|
||||
) { itemState, authCodeState ->
|
||||
val totpData = authCodeState.data?.let {
|
||||
TotpCodeItemData(
|
||||
periodSeconds = it.periodSeconds,
|
||||
timeLeftSeconds = it.timeLeftSeconds,
|
||||
totpCode = it.totpCode,
|
||||
verificationCode = it.code
|
||||
)
|
||||
}
|
||||
authenticatorRepository.getAuthCodeFlow(state.itemId),
|
||||
settingsRepository.authenticatorAlertThresholdSecondsFlow,
|
||||
) { itemState, authCodeState, alertThresholdSeconds ->
|
||||
|
||||
ItemAction.Internal.ItemDataReceive(
|
||||
item = itemState.data,
|
||||
itemDataState = combineDataStates(itemState, authCodeState) { item, _ ->
|
||||
itemDataState = combineDataStates(
|
||||
itemState,
|
||||
authCodeState,
|
||||
) { item, authCode ->
|
||||
val totpData = authCode?.let {
|
||||
TotpCodeItemData(
|
||||
periodSeconds = it.periodSeconds,
|
||||
timeLeftSeconds = it.timeLeftSeconds,
|
||||
totpCode = it.totpCode,
|
||||
verificationCode = it.code
|
||||
)
|
||||
}
|
||||
|
||||
ItemData(
|
||||
accountName = itemState.data?.name.orEmpty(),
|
||||
name = item?.name.orEmpty(),
|
||||
alertThresholdSeconds = alertThresholdSeconds,
|
||||
totpCodeItemData = totpData
|
||||
)
|
||||
}
|
||||
@@ -122,7 +131,11 @@ class ItemViewModel @Inject constructor(
|
||||
it.copy(
|
||||
itemId = action.item?.id.orEmpty(),
|
||||
viewState = ItemState.ViewState.Content(
|
||||
totpCodeItemData = totpItemData
|
||||
itemData = ItemData(
|
||||
name = action.item?.name.orEmpty(),
|
||||
alertThresholdSeconds = action.itemDataState.data?.alertThresholdSeconds ?: 0,
|
||||
totpCodeItemData = totpItemData,
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -167,7 +180,7 @@ data class ItemState(
|
||||
*/
|
||||
@Parcelize
|
||||
data class Content(
|
||||
val totpCodeItemData: TotpCodeItemData,
|
||||
val itemData: ItemData,
|
||||
) : ViewState()
|
||||
}
|
||||
|
||||
|
||||
@@ -6,11 +6,14 @@ import kotlinx.parcelize.Parcelize
|
||||
/**
|
||||
* Represents the item data displayed to users.
|
||||
*
|
||||
* @property accountName Name of the account associated to the item.
|
||||
* @property name Name of the account associated to the item.
|
||||
* @property alertThresholdSeconds Threshold, in seconds, at which an Item is considered near
|
||||
* expiration.
|
||||
* @property totpCodeItemData TOTP data for the account.
|
||||
*/
|
||||
@Parcelize
|
||||
data class ItemData(
|
||||
val accountName: String,
|
||||
val name: String,
|
||||
val alertThresholdSeconds: Int,
|
||||
val totpCodeItemData: TotpCodeItemData?,
|
||||
) : Parcelable
|
||||
|
||||
@@ -111,19 +111,20 @@ fun ItemListingScreen(
|
||||
LazyColumn {
|
||||
items(currentState.itemList) {
|
||||
VaultVerificationCodeItem(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
startIcon = it.startIcon,
|
||||
label = it.label,
|
||||
supportingLabel = it.supportingLabel,
|
||||
timeLeftSeconds = it.timeLeftSeconds,
|
||||
periodSeconds = it.periodSeconds,
|
||||
alertThresholdSeconds = it.alertThresholdSeconds,
|
||||
authCode = it.authCode,
|
||||
onCopyClick = { /*TODO*/ },
|
||||
onItemClick = {
|
||||
onNavigateToItemScreen(it.id)
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,12 +5,15 @@ import androidx.lifecycle.viewModelScope
|
||||
import com.x8bit.bitwarden.authenticator.R
|
||||
import com.x8bit.bitwarden.authenticator.data.authenticator.manager.model.VerificationCodeItem
|
||||
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.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.components.model.IconData
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.update
|
||||
@@ -23,6 +26,7 @@ import javax.inject.Inject
|
||||
@HiltViewModel
|
||||
class ItemListingViewModel @Inject constructor(
|
||||
authenticatorRepository: AuthenticatorRepository,
|
||||
settingsRepository: SettingsRepository,
|
||||
) :
|
||||
BaseViewModel<ItemListingState, ItemListingEvent, ItemListingAction>(
|
||||
initialState = ItemListingState(
|
||||
@@ -32,7 +36,53 @@ class ItemListingViewModel @Inject constructor(
|
||||
) {
|
||||
|
||||
init {
|
||||
authenticatorRepository.getAuthCodesFlow()
|
||||
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))
|
||||
}
|
||||
@@ -59,16 +109,16 @@ class ItemListingViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
private fun handleAuthenticatorDataReceive(action: ItemListingAction.Internal.AuthenticatorDataReceive) {
|
||||
when (val authenticatorData = action.authenticatorData) {
|
||||
is DataState.Error -> authenticatorErrorReceive(authenticatorData)
|
||||
is DataState.Loaded -> authenticatorDataLoadedReceive(authenticatorData)
|
||||
DataState.Loading -> authenticatorDataLoadingReceive(authenticatorData)
|
||||
is DataState.NoNetwork -> authenticatorNoNetworkReceive(authenticatorData)
|
||||
when (val viewState = action.itemListingDataState) {
|
||||
is DataState.Error -> authenticatorErrorReceive(viewState)
|
||||
is DataState.Loaded -> authenticatorDataLoadedReceive(viewState.data)
|
||||
is DataState.Loading -> authenticatorDataLoadingReceive()
|
||||
is DataState.NoNetwork -> authenticatorNoNetworkReceive(viewState.data)
|
||||
is DataState.Pending -> authenticatorPendingReceive()
|
||||
}
|
||||
}
|
||||
|
||||
private fun authenticatorErrorReceive(authenticatorData: DataState<List<VerificationCodeItem>>) {
|
||||
private fun authenticatorErrorReceive(authenticatorData: DataState.Error<ItemListingData>) {
|
||||
mutableStateFlow.update {
|
||||
if (authenticatorData.data != null) {
|
||||
val viewState = updateViewState(authenticatorData.data)
|
||||
@@ -90,31 +140,31 @@ class ItemListingViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun authenticatorDataLoadedReceive(authenticatorData: DataState<List<VerificationCodeItem>>) {
|
||||
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.data),
|
||||
viewState = updateViewState(authenticatorData),
|
||||
dialog = null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun authenticatorDataLoadingReceive(authenticatorData: DataState<List<VerificationCodeItem>>) {
|
||||
private fun authenticatorDataLoadingReceive() {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
viewState = updateViewState(authenticatorData.data),
|
||||
viewState = ItemListingState.ViewState.Loading,
|
||||
dialog = ItemListingState.DialogState.Syncing
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun authenticatorNoNetworkReceive(authenticatorData: DataState<List<VerificationCodeItem>>) {
|
||||
private fun authenticatorNoNetworkReceive(authenticatorData: ItemListingData?) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
viewState = updateViewState(authenticatorData.data),
|
||||
viewState = updateViewState(authenticatorData),
|
||||
dialog = ItemListingState.DialogState.Error(
|
||||
title = R.string.internet_connection_required_title.asText(),
|
||||
message = R.string.internet_connection_required_message.asText(),
|
||||
@@ -131,12 +181,14 @@ class ItemListingViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateViewState(verificationCodeItems: List<VerificationCodeItem>?) =
|
||||
if (verificationCodeItems.isNullOrEmpty()) {
|
||||
ItemListingState.ViewState.NoItems
|
||||
} else {
|
||||
ItemListingState.ViewState.Content(verificationCodeItems.toDisplayItems())
|
||||
}
|
||||
private fun updateViewState(
|
||||
itemListingData: ItemListingData?,
|
||||
): ItemListingState.ViewState {
|
||||
val items = itemListingData?.authenticatorData ?: return ItemListingState.ViewState.NoItems
|
||||
return ItemListingState.ViewState.Content(
|
||||
itemList = items.toDisplayItems(itemListingData.alertThresholdSeconds)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -285,7 +337,7 @@ sealed class ItemListingAction {
|
||||
* Indicates authenticator item listing data has been received.
|
||||
*/
|
||||
data class AuthenticatorDataReceive(
|
||||
val authenticatorData: DataState<List<VerificationCodeItem>>,
|
||||
val itemListingDataState: DataState<ItemListingData>,
|
||||
) : Internal()
|
||||
}
|
||||
}
|
||||
@@ -300,11 +352,12 @@ data class VerificationCodeDisplayItem(
|
||||
val supportingLabel: String?,
|
||||
val timeLeftSeconds: Int,
|
||||
val periodSeconds: Int,
|
||||
val alertThresholdSeconds: Int,
|
||||
val authCode: String,
|
||||
val startIcon: IconData = IconData.Local(R.drawable.ic_login_item),
|
||||
) : Parcelable
|
||||
|
||||
private fun List<VerificationCodeItem>.toDisplayItems() = this.map {
|
||||
private fun List<VerificationCodeItem>.toDisplayItems(alertThresholdSeconds: Int) = this.map {
|
||||
VerificationCodeDisplayItem(
|
||||
id = it.id,
|
||||
label = it.name,
|
||||
@@ -312,5 +365,6 @@ private fun List<VerificationCodeItem>.toDisplayItems() = this.map {
|
||||
supportingLabel = it.username,
|
||||
periodSeconds = it.periodSeconds,
|
||||
timeLeftSeconds = it.timeLeftSeconds,
|
||||
alertThresholdSeconds = alertThresholdSeconds
|
||||
)
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@ fun VaultVerificationCodeItem(
|
||||
label: String,
|
||||
periodSeconds: Int,
|
||||
timeLeftSeconds: Int,
|
||||
alertThresholdSeconds: Int,
|
||||
startIcon: IconData,
|
||||
onCopyClick: () -> Unit,
|
||||
onItemClick: () -> Unit,
|
||||
@@ -101,6 +102,7 @@ fun VaultVerificationCodeItem(
|
||||
BitwardenCircularCountdownIndicator(
|
||||
timeLeftSeconds = timeLeftSeconds,
|
||||
periodSeconds = periodSeconds,
|
||||
alertThresholdSeconds = alertThresholdSeconds
|
||||
)
|
||||
|
||||
Text(
|
||||
@@ -137,6 +139,7 @@ private fun VerificationCodeItem_preview() {
|
||||
onCopyClick = {},
|
||||
onItemClick = {},
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
alertThresholdSeconds = 7
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.x8bit.bitwarden.authenticator.ui.authenticator.feature.itemlisting.model
|
||||
|
||||
import com.x8bit.bitwarden.authenticator.data.authenticator.manager.model.VerificationCodeItem
|
||||
|
||||
data class ItemListingData(
|
||||
val alertThresholdSeconds: Int,
|
||||
val authenticatorData: List<VerificationCodeItem>?,
|
||||
)
|
||||
@@ -12,6 +12,7 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.StrokeCap
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@@ -24,9 +25,11 @@ import androidx.compose.ui.unit.dp
|
||||
*/
|
||||
@Composable
|
||||
fun BitwardenCircularCountdownIndicator(
|
||||
modifier: Modifier = Modifier,
|
||||
timeLeftSeconds: Int,
|
||||
periodSeconds: Int,
|
||||
modifier: Modifier = Modifier,
|
||||
alertThresholdSeconds: Int = -1,
|
||||
alertIndicatorColor: Color = MaterialTheme.colorScheme.error,
|
||||
) {
|
||||
val progressAnimate by animateFloatAsState(
|
||||
targetValue = timeLeftSeconds.toFloat() / periodSeconds,
|
||||
@@ -45,7 +48,11 @@ fun BitwardenCircularCountdownIndicator(
|
||||
CircularProgressIndicator(
|
||||
progress = { progressAnimate },
|
||||
modifier = Modifier.size(size = 30.dp),
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
color = if (timeLeftSeconds > alertThresholdSeconds) {
|
||||
MaterialTheme.colorScheme.primary
|
||||
} else {
|
||||
alertIndicatorColor
|
||||
},
|
||||
strokeWidth = 3.dp,
|
||||
strokeCap = StrokeCap.Round,
|
||||
)
|
||||
@@ -53,7 +60,11 @@ fun BitwardenCircularCountdownIndicator(
|
||||
Text(
|
||||
text = timeLeftSeconds.toString(),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
color = if (timeLeftSeconds > alertThresholdSeconds) {
|
||||
MaterialTheme.colorScheme.onSurfaceVariant
|
||||
} else {
|
||||
alertIndicatorColor
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<string name="add_item">Add item</string>
|
||||
<string name="an_error_has_occurred">An error has occurred.</string>
|
||||
<string name="generic_error_message">We were unable to process your request. Please try again or contact us.</string>
|
||||
<string name="syncing_complete">SyncingComplete</string>
|
||||
<string name="syncing_complete">Syncing Complete</string>
|
||||
<string name="internet_connection_required_title">Internet connection required</string>
|
||||
<string name="internet_connection_required_message">Please connect to the internet before continuing.</string>
|
||||
<string name="ok">OK</string>
|
||||
@@ -17,4 +17,5 @@
|
||||
<string name="close">Close</string>
|
||||
<string name="view_item">View Item</string>
|
||||
<string name="bitwarden_authenticator">Bitwarden Authenticator</string>
|
||||
</resources>
|
||||
<string name="name">Name</string>
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user