From 8cd0894aff55f5f7d46be60b9853639d6ff7d5c5 Mon Sep 17 00:00:00 2001 From: Andre Rosado Date: Mon, 27 Apr 2026 13:06:44 +0100 Subject: [PATCH] [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) --- .../itemlisting/ItemListingViewModel.kt | 12 ++++-- .../util/VerificationCodeItemExtensions.kt | 13 +++++-- .../itemlisting/ItemListingViewModelTest.kt | 2 +- .../VerificationCodeItemExtensionsTest.kt | 39 +++++++++++++++++-- 4 files changed, 54 insertions(+), 12 deletions(-) diff --git a/authenticator/src/main/kotlin/com/bitwarden/authenticator/ui/authenticator/feature/itemlisting/ItemListingViewModel.kt b/authenticator/src/main/kotlin/com/bitwarden/authenticator/ui/authenticator/feature/itemlisting/ItemListingViewModel.kt index 3f6821aa09..c7bf82c5ca 100644 --- a/authenticator/src/main/kotlin/com/bitwarden/authenticator/ui/authenticator/feature/itemlisting/ItemListingViewModel.kt +++ b/authenticator/src/main/kotlin/com/bitwarden/authenticator/ui/authenticator/feature/itemlisting/ItemListingViewModel.kt @@ -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) diff --git a/authenticator/src/main/kotlin/com/bitwarden/authenticator/ui/authenticator/feature/util/VerificationCodeItemExtensions.kt b/authenticator/src/main/kotlin/com/bitwarden/authenticator/ui/authenticator/feature/util/VerificationCodeItemExtensions.kt index 40554a804c..75edeb0039 100644 --- a/authenticator/src/main/kotlin/com/bitwarden/authenticator/ui/authenticator/feature/util/VerificationCodeItemExtensions.kt +++ b/authenticator/src/main/kotlin/com/bitwarden/authenticator/ui/authenticator/feature/util/VerificationCodeItemExtensions.kt @@ -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) { diff --git a/authenticator/src/test/kotlin/com/bitwarden/authenticator/ui/authenticator/feature/itemlisting/ItemListingViewModelTest.kt b/authenticator/src/test/kotlin/com/bitwarden/authenticator/ui/authenticator/feature/itemlisting/ItemListingViewModelTest.kt index 2e98ad3436..9c5a89dab1 100644 --- a/authenticator/src/test/kotlin/com/bitwarden/authenticator/ui/authenticator/feature/itemlisting/ItemListingViewModelTest.kt +++ b/authenticator/src/test/kotlin/com/bitwarden/authenticator/ui/authenticator/feature/itemlisting/ItemListingViewModelTest.kt @@ -582,7 +582,7 @@ class ItemListingViewModelTest : BaseViewModelTest() { val verificationItem = VerificationCodeItem( code = "123456", periodSeconds = 60, - timeLeftSeconds = 430, + timeLeftSeconds = 5, issueTime = 35L, id = "1", issuer = "issuer", diff --git a/authenticator/src/test/kotlin/com/bitwarden/authenticator/ui/authenticator/feature/util/VerificationCodeItemExtensionsTest.kt b/authenticator/src/test/kotlin/com/bitwarden/authenticator/ui/authenticator/feature/util/VerificationCodeItemExtensionsTest.kt index 996dd10a46..6edcb9f1e6 100644 --- a/authenticator/src/test/kotlin/com/bitwarden/authenticator/ui/authenticator/feature/util/VerificationCodeItemExtensionsTest.kt +++ b/authenticator/src/test/kotlin/com/bitwarden/authenticator/ui/authenticator/feature/util/VerificationCodeItemExtensionsTest.kt @@ -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,