[PM-41929] fix: Update the manage devices screen when a passwordless request push is received - #7280
[PM-41929] fix: Update the manage devices screen when a passwordless request push is received#7280aj-rosado wants to merge 4 commits into
Conversation
🤖 Bitwarden Claude Code ReviewOverall Assessment: APPROVE Reviewed the full PR and, in particular, the changes since the previous review ( Code Review DetailsNo findings at or above the reporting threshold. Verification notes:
|
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #7280 +/- ##
==========================================
- Coverage 86.25% 86.02% -0.23%
==========================================
Files 891 976 +85
Lines 65294 67571 +2277
Branches 9808 9916 +108
==========================================
+ Hits 56320 58130 +1810
- Misses 5472 5951 +479
+ Partials 3502 3490 -12
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
| /** | ||
| * Whether this request may still be approved or declined, meaning it has not already been | ||
| * approved, not been declined (indicated by it not being approved & having a responseDate), | ||
| * and has not expired (it is under 5 minutes old). | ||
| */ | ||
| private val AuthRequest.isActionable: Boolean | ||
| get() = !requestApproved && | ||
| responseDate == null && | ||
| !creationDate.isOverFiveMinutesOld(clock) |
There was a problem hiding this comment.
♻️ DEBT: isActionable is now a third copy of the approved/declined/expired predicate.
Details and fix
The same three-clause rule already exists in two places:
ManageDevicesViewModel.kt:537—List<AuthRequest>.filterRespondedAndExpired(clock)PendingRequestsViewModel.kt:401— identicalfilterRespondedAndExpired(clock)
Adding a third copy here means the five-minute window and the decline detection now have to be kept in sync across three files.
Since this PR already extracts toAuthRequest into data/auth/manager/util/, consider putting the predicate there too and having both view models' filterRespondedAndExpired delegate to it:
// data/auth/manager/util/AuthRequestExtensions.kt
val AuthRequest.isActionable: Boolean
get() = !requestApproved &&
responseDate == null &&
!creationDate.isOverFiveMinutesOld(clock)(The clock would need to become a parameter, matching how filterRespondedAndExpired already takes one.)
Non-blocking — the current behavior is correct and matches the existing copies.
| publicKey = initialAuthRequest.publicKey, | ||
| fingerprint = initialAuthRequest.fingerprint, | ||
| ) | ||
| .toAuthRequest(fingerprint = initialAuthRequest.fingerprint) |
There was a problem hiding this comment.
Can we just pass these values into the extension method instead of copying it.
fun AuthRequestsResponseJson.AuthRequest.toAuthRequest(
fingerprint: String,
publicKey: String = this.publicKey,
responseDate: Instant = this.responseDate,
isRequestApproved: Boolean = this.requestApproved,
): AuthRequest = AuthRequest(| fun AuthRequestsResponseJson.AuthRequest.toAuthRequest( | ||
| fingerprint: String, | ||
| ): AuthRequest = AuthRequest( | ||
| id = id, |
There was a problem hiding this comment.
For clarity, can you dd the explicit this to all of these.
| // The device list is the only source that reports which device owns a pending request, so | ||
| // it is re-read before the new request can be rendered against its device. | ||
| viewModelScope.launch { | ||
| sendAction( |
There was a problem hiding this comment.
Can we just call a common method from both here and from handlePasswordlessAuthRequestDevicesReceive?
There was a problem hiding this comment.
Am I missing something or is the PasswordlessAuthRequestDevicesReceive action only ever launched from here?
If so, why do we need to actions like this?
There was a problem hiding this comment.
OK, I finally go there, the authRepository.getDevices() is suspending.
All of this is fine 😄
There was a problem hiding this comment.
I have simplified it a bit by moving getDevices into a map on the getPasswordlessAuthRequestFlow
| @@ -399,8 +399,4 @@ sealed class PendingRequestsAction { | |||
| * * The request has expired (it is at least 5 minutes old). | |||
| */ | |||
| private fun List<AuthRequest>.filterRespondedAndExpired(clock: Clock) = | |||
There was a problem hiding this comment.
Looks like this function exists in 2 spots. What do you think about consolidating them in the AuthRequestExtenstions file?
| init { | ||
| updateAuthRequestList() | ||
| fetchAllDevices() | ||
| observePasswordlessAuthRequests() |
There was a problem hiding this comment.
Instead of observing this separately, should getAuthRequestsWithUpdates called in updateAuthRequestList just observer the push notifications directly?
Is there any reason not to do this?
There was a problem hiding this comment.
observePasswordlessAuthRequests Will only get the authRequest returned by the push instead of the whole list
There was a problem hiding this comment.
Is there a reason we would not want to live update the getAuthRequestsWithUpdates flow with data from a push?
There was a problem hiding this comment.
no reason, was simply my choice to try to minimize the network call, although looking at it now we might benefit from getting the whole list as right now it will not update expired requests (or other new requests) and the impact would be minimal
There was a problem hiding this comment.
I have approved and you are free to merge but I do agree there would be benefits to merging the push notifications into the other flow. It would make this ViewModel much simplier and improve the speed at which getAuthRequestsWithUpdates gets updates, which is good for this screen and other places in the app.
Something to consider.
| * * The request has been declined (indicated by it not being approved & having a responseDate). | ||
| * * The request has expired (it is at least 5 minutes old). | ||
| */ | ||
| fun List<AuthRequest>.filterRespondedAndExpired(clock: Clock): List<AuthRequest> = |
🎟️ Tracking
https://bitwarden.atlassian.net/browse/PM-41929
📔 Objective
The Manage Devices screen only learned about pending login requests when it polled or when the
user pulled to refresh, so a request initiated while the screen was already open did not appear
until the next poll cycle.
This wires the push notification stream into the screen:
AuthRequestManager.getPasswordlessAuthRequestFlow()observesPushManager.passwordlessRequestFlow,filters to the active user (a push for another user would otherwise be hydrated with the active
user's token), hydrates the request with its fingerprint, and emits only requests that are still
actionable — not already approved, not declined, and under five minutes old. Requests that fail
to hydrate are logged and dropped rather than surfaced as errors.
ManageDevicesViewModelcollects that flow, re-reads the device list (the only source thatreports which device owns a pending request), and merges the request into state, replacing any
earlier copy so it cannot be listed twice. Because the refresh is not user-initiated, a failed
device fetch leaves the screen untouched instead of replacing it with an error — polling and
pull-to-refresh reconcile it later.
Also extracts the repeated
AuthRequestsResponseJson.AuthRequest→AuthRequestmapping into atoAuthRequest(fingerprint)extension, replacing seven hand-written copies inAuthRequestManagerImplwith no behavior change.