-
Notifications
You must be signed in to change notification settings - Fork 917
[PM-28835] Added validations to prevent duplicate press on buttons #6209
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
Conversation
|
Claude finished @aj-rosado's task —— View job Code Review CompleteTasks
Review SummaryStatus: ❌ Changes Requested All previous review comments from David have been successfully addressed. However, I've identified a critical race condition in the throttling implementation that needs to be fixed before merge, along with some API design improvements. Critical Issues1. Race Condition in Throttle Implementation ❌ CRITICALLocation: The current implementation has a race condition where multiple rapid clicks can bypass throttling: var isEnabled by remember { mutableStateOf(value = true) }
return {
coroutineScope.launch {
if (isEnabled) { // ← Multiple coroutines can pass this check
isEnabled = false // ← before any set it to false
onClick()
delay(timeMillis = delayMs)
isEnabled = true
}
}
}Problem: The check-then-set pattern is not atomic. If a user clicks twice within microseconds, both clicks might see Impact: This could still allow duplicate save operations, which is what this PR aims to prevent. Recommended Fix: Use @Composable
fun throttledClick(
onClick: () -> Unit,
delayMs: Long = 300,
coroutineScope: CoroutineScope = rememberCoroutineScope(),
): () -> Unit {
val mutex = remember { Mutex() }
return {
coroutineScope.launch {
if (mutex.tryLock()) {
try {
onClick()
delay(timeMillis = delayMs)
} finally {
mutex.unlock()
}
}
}
}
}Don't forget to add: 2. Parameter Ordering Violates Kotlin Conventions
|
| return@onContent | ||
| } | ||
|
|
||
| if (state.dialog is FolderAddEditState.DialogState.Loading) return@onContent |
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.
💭 Consider guard clause placement for consistency
Details
The loading state guard is placed between validation and state update. While functionally correct, the other ViewModels in this PR (AddEditSendViewModel:561, VaultAddEditViewModel:403) place the guard immediately before state mutation, after all validations are complete.
Current pattern:
if (content.folderName.isEmpty()) {
// validation error
return@onContent
}
if (state.dialog is FolderAddEditState.DialogState.Loading) return@onContent // Line 106
mutableStateFlow.update { ... }Consistent pattern (optional):
if (content.folderName.isEmpty()) {
// validation error
return@onContent
}
// Any other validations...
if (state.dialog is FolderAddEditState.DialogState.Loading) return@onContent
mutableStateFlow.update { ... }This is a minor stylistic point - the current implementation is correct and safe.
|
Great job! No new security vulnerabilities introduced in this pull request |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #6209 +/- ##
==========================================
- Coverage 85.41% 85.38% -0.03%
==========================================
Files 755 755
Lines 54114 54109 -5
Branches 7803 7795 -8
==========================================
- Hits 46220 46203 -17
- Misses 5180 5195 +15
+ Partials 2714 2711 -3 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
| ) | ||
| } | ||
|
|
||
| @Suppress("MaxLineLength") |
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 suppression is not needed
| } returns mockProviderCreateCredentialRequest | ||
| } | ||
|
|
||
| @Suppress("MaxLineLength") |
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 don't need this suppression
| onClick = remember(viewModel) { | ||
| { viewModel.trySendAction(VaultAddEditAction.Common.SaveClick) } | ||
| }, | ||
| isEnabled = state.dialog !is VaultAddEditState.DialogState.Loading, |
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.
Realistically, can't we just say:
isEnabled = state.dialog == null,| onClick = remember(viewModel) { | ||
| { viewModel.trySendAction(FolderAddEditAction.SaveClick) } | ||
| }, | ||
| isEnabled = state.dialog !is FolderAddEditState.DialogState.Loading, |
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.
Thoughts on handling this inside the button? Adding some throttling inside the various BitwardenButtons?
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 have tested this and just before navigating back I am able to create a duplicate item if I am spamming the save button.
On current main I can even create two duplicates. The throttle mitigates this a little bit but is not as consistent as current approach.
However... I believe throttle would solve the real world scenarios where this happens
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.
🤔 should we keep this for save operations (in order to prevent duplicates) but still add throttling for all the buttons?
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.
If the throttling works, I don't think we need to add this extra code, right?
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 was playing around with this and something like this should work and we can generically apply it to all of the button components:
@Composable
fun throttledClick(
coroutineScope: CoroutineScope = rememberCoroutineScope(),
delayMs: Long = 300,
onClick: () -> Unit,
): () -> Unit {
var isEnabled by remember { mutableStateOf(value = true) }
return {
coroutineScope.launch {
if (isEnabled) {
isEnabled = false
onClick()
delay(timeMillis = delayMs)
isEnabled = true
}
}
}
}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.
It does not completely fix it, I have been able to create a duplicate. But unless someone intentionally tries to do it should be fine.
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.
👍
If this bug ever re-appears, we can always increase the delay, but I am hesitant to go much higher than ~300
ui/src/main/kotlin/com/bitwarden/ui/platform/components/util/ThrottledClickHandler.kt
Outdated
Show resolved
Hide resolved
ui/src/main/kotlin/com/bitwarden/ui/platform/components/util/ThrottledClick.kt
Show resolved
Hide resolved
ui/src/main/kotlin/com/bitwarden/ui/platform/components/util/ThrottledClickHandler.kt
Outdated
Show resolved
Hide resolved
app/src/main/kotlin/com/x8bit/bitwarden/ui/tools/feature/send/addedit/AddEditSendViewModel.kt
Outdated
Show resolved
Hide resolved
| } | ||
| return@onContent | ||
| } | ||
|
|
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 drop these unnecessary changes
| ProviderCreateCredentialRequest.fromBundle(any()) | ||
| } returns mockProviderCreateCredentialRequest | ||
| } | ||
|
|
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.
Ditto
| * @param onClick The action to perform when clicked. | ||
| * @return A throttled click handler function. | ||
| */ | ||
|
|
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 drop this newline
| @@ -0,0 +1,39 @@ | |||
| package com.bitwarden.ui.platform.components.util | |||
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 rename the file to match the function: ThrottledClick
…o match the function
|
Thank you @david-livefront |

🎟️ Tracking
https://bitwarden.atlassian.net/browse/PM-28835
📔 Objective
On some scenarios if the user tried to multi press the button he can create multiple entries by mistake. This address that issue on item, folder and send creation and also fixes multiple calls to remove password.
⏰ Reminders before review
🦮 Reviewer guidelines
:+1:) or similar for great changes:memo:) or ℹ️ (:information_source:) for notes or general info:question:) for questions:thinking:) or 💭 (:thought_balloon:) for more open inquiry that's not quite a confirmed issue and could potentially benefit from discussion:art:) for suggestions / improvements:x:) or:warning:) for more significant problems or concerns needing attention:seedling:) or ♻️ (:recycle:) for future improvements or indications of technical debt:pick:) for minor or nitpick changes