Skip to content

Commit

Permalink
Change color of countdown indicator when period nears expiration
Browse files Browse the repository at this point in the history
  • Loading branch information
SaintPatrck committed Mar 28, 2024
1 parent 0a73a17 commit 83e5f3d
Show file tree
Hide file tree
Showing 13 changed files with 215 additions and 47 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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].
Expand All @@ -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 ->
Expand All @@ -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")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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>
}
Original file line number Diff line number Diff line change
Expand Up @@ -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

/**
Expand All @@ -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
Expand All @@ -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(),
)

}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -123,20 +124,32 @@ 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 = { },
readOnly = true,
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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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(
Expand All @@ -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
)
}
Expand Down Expand Up @@ -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,
)
)
)
}
Expand Down Expand Up @@ -167,7 +180,7 @@ data class ItemState(
*/
@Parcelize
data class Content(
val totpCodeItemData: TotpCodeItemData,
val itemData: ItemData,
) : ViewState()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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),
)
}
}
Expand Down
Loading

0 comments on commit 83e5f3d

Please sign in to comment.