[BWA-99] fix: Show next code only in last 10 seconds; fix flow caching bug

- Cache getLocalVerificationCodesFlow() as a class property so
  handleShowNextTotpCodeReceive reads from the already-subscribed
  flow instead of a new unsubscribed instance that always returns
  DataState.Loading (which caused the next code to never appear)
- Add 10-second threshold: next code is only shown when timeLeftSeconds
  is at or below 10 seconds remaining in the current period
- Update tests for the threshold (timeLeftSeconds = 5 / 10 / 11 cases)
This commit is contained in:
Andre Rosado
2026-04-27 13:06:44 +01:00
parent 380dcaddbb
commit 8cd0894aff
4 changed files with 54 additions and 12 deletions

View File

@@ -70,6 +70,10 @@ class ItemListingViewModel @Inject constructor(
),
) {
// Cached once so that handleShowNextTotpCodeReceive can read `.value` from the
// already-subscribed flow rather than creating a new unsubscribed instance.
private val localCodesFlow = authenticatorRepository.getLocalVerificationCodesFlow()
init {
settingsRepository
.authenticatorAlertThresholdSecondsFlow
@@ -84,7 +88,7 @@ class ItemListingViewModel @Inject constructor(
.launchIn(viewModelScope)
combine(
flow = authenticatorRepository.getLocalVerificationCodesFlow(),
flow = localCodesFlow,
flow2 = authenticatorRepository.sharedCodesStateFlow,
ItemListingAction.Internal::AuthCodesUpdated,
)
@@ -467,10 +471,10 @@ class ItemListingViewModel @Inject constructor(
mutableStateFlow.update {
it.copy(showNextTotpCode = action.showNextTotpCode)
}
// Re-derive the displayed list so that the next code is shown/hidden immediately
// when the user toggles the setting.
// Immediately re-derive the displayed list using the cached subscribed flow so that
// next-code visibility changes take effect without waiting for the next 1-second tick.
val codesUpdate = ItemListingAction.Internal.AuthCodesUpdated(
localCodes = authenticatorRepository.getLocalVerificationCodesFlow().value,
localCodes = localCodesFlow.value,
sharedCodesState = authenticatorRepository.sharedCodesStateFlow.value,
)
handleAuthenticatorDataReceive(codesUpdate)

View File

@@ -5,12 +5,15 @@ import com.bitwarden.authenticator.data.authenticator.repository.model.Authentic
import com.bitwarden.authenticator.data.authenticator.repository.model.SharedVerificationCodesState
import com.bitwarden.authenticator.ui.platform.components.listitem.model.VerificationCodeDisplayItem
private const val NEXT_CODE_SHOW_THRESHOLD_SECONDS = 10
/**
* Converts [VerificationCodeItem] to a [VerificationCodeDisplayItem].
*
* @param showNextCode When `true`, the upcoming code is mapped to
* [VerificationCodeDisplayItem.nextAuthCode]. When `false`, [VerificationCodeDisplayItem.nextAuthCode]
* is always `null`.
* [VerificationCodeDisplayItem.nextAuthCode] for items within the last
* [NEXT_CODE_SHOW_THRESHOLD_SECONDS] seconds of their validity period. When `false`,
* [VerificationCodeDisplayItem.nextAuthCode] is always `null`.
*/
fun VerificationCodeItem.toDisplayItem(
alertThresholdSeconds: Int,
@@ -30,7 +33,11 @@ fun VerificationCodeItem.toDisplayItem(
periodSeconds = periodSeconds,
alertThresholdSeconds = alertThresholdSeconds,
authCode = code,
nextAuthCode = if (showNextCode) nextCode else null,
nextAuthCode = if (showNextCode && timeLeftSeconds <= NEXT_CODE_SHOW_THRESHOLD_SECONDS) {
nextCode
} else {
null
},
showOverflow = showOverflow,
favorite = (source as? AuthenticatorItem.Source.Local)?.isFavorite ?: false,
showMoveToBitwarden = when (source) {

View File

@@ -582,7 +582,7 @@ class ItemListingViewModelTest : BaseViewModelTest() {
val verificationItem = VerificationCodeItem(
code = "123456",
periodSeconds = 60,
timeLeftSeconds = 430,
timeLeftSeconds = 5,
issueTime = 35L,
id = "1",
issuer = "issuer",

View File

@@ -146,8 +146,9 @@ class VerificationCodeItemExtensionsTest {
}
@Test
fun `toDisplayItem should set nextAuthCode to nextCode when showNextCode is true`() {
val item = createMockVerificationCodeItem(number = 1, nextCode = "654321")
@Suppress("MaxLineLength")
fun `toDisplayItem should set nextAuthCode when showNextCode is true and timeLeftSeconds is within threshold`() {
val item = createMockVerificationCodeItem(number = 1, nextCode = "654321", timeLeftSeconds = 5)
val result = item.toDisplayItem(
alertThresholdSeconds = 7,
sharedVerificationCodesState = SharedVerificationCodesState.Error,
@@ -157,9 +158,39 @@ class VerificationCodeItemExtensionsTest {
assertEquals("654321", result.nextAuthCode)
}
@Test
@Suppress("MaxLineLength")
fun `toDisplayItem should set nextAuthCode when showNextCode is true and timeLeftSeconds equals threshold`() {
val item = createMockVerificationCodeItem(number = 1, nextCode = "654321", timeLeftSeconds = 10)
val result = item.toDisplayItem(
alertThresholdSeconds = 7,
sharedVerificationCodesState = SharedVerificationCodesState.Error,
showOverflow = true,
showNextCode = true,
)
assertEquals("654321", result.nextAuthCode)
}
@Test
@Suppress("MaxLineLength")
fun `toDisplayItem should set nextAuthCode to null when showNextCode is true but timeLeftSeconds exceeds threshold`() {
val item = createMockVerificationCodeItem(number = 1, nextCode = "654321", timeLeftSeconds = 11)
val result = item.toDisplayItem(
alertThresholdSeconds = 7,
sharedVerificationCodesState = SharedVerificationCodesState.Error,
showOverflow = true,
showNextCode = true,
)
assertEquals(null, result.nextAuthCode)
}
@Test
fun `toDisplayItem should set nextAuthCode to null when showNextCode is false`() {
val item = createMockVerificationCodeItem(number = 1, nextCode = "654321")
val item = createMockVerificationCodeItem(
number = 1,
nextCode = "654321",
timeLeftSeconds = 5,
)
val result = item.toDisplayItem(
alertThresholdSeconds = 7,
sharedVerificationCodesState = SharedVerificationCodesState.Error,
@@ -172,7 +203,7 @@ class VerificationCodeItemExtensionsTest {
@Test
@Suppress("MaxLineLength")
fun `toDisplayItem should set nextAuthCode to null when showNextCode true but nextCode is null`() {
val item = createMockVerificationCodeItem(number = 1, nextCode = null)
val item = createMockVerificationCodeItem(number = 1, nextCode = null, timeLeftSeconds = 5)
val result = item.toDisplayItem(
alertThresholdSeconds = 7,
sharedVerificationCodesState = SharedVerificationCodesState.Error,