-
Notifications
You must be signed in to change notification settings - Fork 932
[PM-29309] [BWA-209] Fix TOTP countdown freeze when returning to Authenticator app (change Flow to StateFlow) #6246
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
|
Thank you for your contribution! We've added this to our internal tracking system for review. Details on our contribution process can be found here: https://contributing.bitwarden.com/contributing/pull-requests/community-pr-process. |
Refactor TotpCodeManager to use per-item StateFlow caching, matching the Password Manager's pattern. This prevents flow recreation on resubscribe and removes the 5-second stop timeout that caused stale state when returning from background. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
SaintPatrck
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for contributing, @tajchert.
I've left a few blocking concerns that need to be addressed.
Since you are using Claude to assist with development, would you be open to allowing our Claude agent to review your changes alongside us?
| delay(ONE_SECOND_MILLISECOND) | ||
| .stateIn( | ||
| scope = itemScope, | ||
| started = SharingStarted.Eagerly, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this has the potential to cause leaks. Can we use WhileSubscribed()? That appears to be working in the PasswordManager version.
| var verificationCodeItem: VerificationCodeItem? = null | ||
|
|
||
| while (currentCoroutineContext().isActive) { | ||
| val time = (clock.millis() / ONE_SECOND_MILLISECOND).toInt() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The PasswordManager version gets dateTime and time differently. Can you update this to follow it? It will also give you an instance of DateTime so that you don't have to create one inline, below.
| .getOrNull() | ||
| ?.let { response -> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we use onSuccess and onFailure here, like we're doing in the PasswordManager version?
| issueTime = clock.millis(), | ||
| id = when (item.source) { | ||
| is AuthenticatorItem.Source.Local -> item.source.cipherId | ||
| is AuthenticatorItem.Source.Shared -> UUID.randomUUID().toString() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We recently introduced a UuidManager to ease testing. Can you use it here instead of UUID directly?
| // Emit item | ||
| emit(verificationCodeItem) | ||
|
|
||
| // Wait one second before heading to the top of the loop: | ||
| delay(ONE_SECOND_MILLISECOND) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These comments are redundant. The code itself already describes what is being done. Comments would only be helpful here if they described why it's being done, which is obvious.
| * | ||
| * This implementation uses per-item [StateFlow] caching to prevent flow recreation on each | ||
| * subscribe, ensuring smooth UI updates when returning from background. The pattern mirrors | ||
| * the Password Manager's [com.x8bit.bitwarden.data.vault.manager.TotpCodeManagerImpl]. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add this to the imports if needed so we don't need the FQN.
| @Suppress("LongMethod") | ||
| private fun createVerificationCodeFlow( | ||
| item: AuthenticatorItem, | ||
| ) = flow<VerificationCodeItem?> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
⛏️
| ) = flow<VerificationCodeItem?> { | |
| ): Flow<VerificationCodeItem?> = flow { |
|
|
||
| @Test | ||
| fun `getTotpCodesFlow should return flow that emits empty list when input list is empty`() = | ||
| fun `getTotpCodesFlow should return StateFlow that emits empty list when input list is empty`() = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is going to trigger detekt. Can you add Suppress("MaxLineLength")?
I recommend setting up detekt as a commit-hook as described in README#setup, or at least running it manually before submitting changes.
| * updated verification codes every second. The StateFlow is cached per-item to prevent | ||
| * recreation on each subscribe, ensuring smooth UI updates when returning from background. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The last sentence reads more like implementation detail. Since the implementation has good KDoc let's remove those details from the interface.
Disclaimer: This PR was created with AI assisted agent (Claude Code) but manually checked myself to my best knowledge and this repository standards. If you feel this is against contribution guidelines (I haven't found anything related to such) feel free to disregard this PR. I honestly believe this is valid PR, and I tested it manually as well.
This fix OTP code freeze when returning to Authenticator app from background - issue #6244
when occasionally user can experience frozen TOTP codes and countdown timers when returning to the Authenticator app from background. The codes remain static and don't update until the screen is manually refreshed. This changes flow implementation in AUthenticator app (Flow) to one that is Password Manager (StateFlow).
Root Cause
The bug stems from a flow lifecycle mismatch in
TotpCodeManagerImpl. The TOTP code update mechanism relies on continuous Flow emissions with a 1-second delay loop, but this flow stops when the app goes to background.The race condition:
collectAsStateWithLifecycle()stops collectingSharingStarted.WhileSubscribed(5_000L)stops upstream flowsdelay(ONE_SECOND_MILLISECOND)timer loop stops entirelystateInvalue briefly displayed before new emissionscombine()waits for all flows to emitSolution
Aligned
TotpCodeManagerImplwith the Password Manager's proven pattern:mutableMapOf<AuthenticatorItem, StateFlow<...>>— Prevents flow recreation on resubscribeSharingStarted.WhileSubscribed()without delaySharingStarted.Eagerlyfor per-item flows — Ensures cached per-item flows continue emitting even when the combined flow changes (e.g., when adding new items)Changes
TotpCodeManager.kt— Changed return type fromFlow<List<VerificationCodeItem>>toStateFlow<List<VerificationCodeItem>>TotpCodeManagerImpl.kt— AddedDispatcherManagerdependency, per-item StateFlow caching withgetOrCreateItemStateFlow(), and cleanup handlersAuthenticatorManagerModule.kt— PassesdispatcherManagertoTotpCodeManagerImplconstructorAuthenticatorRepositoryImpl.kt— Removed 5-second timeout, now usesSharingStarted.WhileSubscribed()without delayMutableStateFlow()instead offlowOf()Testing
I wasn't able to recreate issue after this fix.