Skip to content

[PM-33982] feat: Add device management screen#6754

Open
aj-rosado wants to merge 7 commits intomainfrom
PM-33982/build-device-screen
Open

[PM-33982] feat: Add device management screen#6754
aj-rosado wants to merge 7 commits intomainfrom
PM-33982/build-device-screen

Conversation

@aj-rosado
Copy link
Copy Markdown
Contributor

🎟️ Tracking

https://bitwarden.atlassian.net/browse/PM-33982

📔 Objective

Add a new screen for device management on Android.
This new screen should display the current device, followed by the pending authorization requests and finally all devices with active sessions ordered by "last active date".

📸 Screenshots

Manage_devices_screen Account_security_options_screen

@aj-rosado aj-rosado requested review from a team and david-livefront as code owners April 1, 2026 18:27
@aj-rosado aj-rosado added the innovation Feature work related to Innovation Sprint or BEEEP projects label Apr 1, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 1, 2026

Logo
Checkmarx One – Scan Summary & Detailse840e7cb-a86e-48ca-a472-3b64e913bd0a

Great job! No new security vulnerabilities introduced in this pull request

# Conflicts:
#	app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarNavigation.kt
#	app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarScreen.kt
#	app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarScreenTest.kt
#	core/src/main/kotlin/com/bitwarden/core/data/manager/model/FlagKey.kt
#	ui/src/main/kotlin/com/bitwarden/ui/platform/components/debug/FeatureFlagListItems.kt
#	ui/src/main/res/values/strings.xml
@github-actions github-actions bot added app:password-manager Bitwarden Password Manager app context app:authenticator Bitwarden Authenticator app context t:feature Change Type - Feature Development labels Apr 1, 2026
@aj-rosado aj-rosado added the ai-review-vnext Request a Claude code review using the vNext workflow label Apr 1, 2026
@codecov
Copy link
Copy Markdown

codecov bot commented Apr 1, 2026

Codecov Report

❌ Patch coverage is 88.73720% with 66 lines in your changes missing coverage. Please review.
✅ Project coverage is 83.50%. Comparing base (747d2d5) to head (721cd2f).
⚠️ Report is 5 commits behind head on main.

Files with missing lines Patch % Lines
...countsecurity/managedevices/ManageDevicesScreen.kt 91.73% 12 Missing and 9 partials ⚠️
...ntsecurity/managedevices/ManageDevicesViewModel.kt 92.96% 4 Missing and 10 partials ⚠️
...ecurity/managedevices/util/DeviceTypeExtensions.kt 68.57% 10 Missing and 1 partial ⚠️
...managedevices/util/DeviceLastActivityExtensions.kt 45.45% 4 Missing and 2 partials ⚠️
...tsecurity/managedevices/ManageDevicesNavigation.kt 28.57% 5 Missing ⚠️
...th/repository/util/DeviceResponseJsonExtensions.kt 66.66% 3 Missing and 1 partial ⚠️
...ttings/accountsecurity/AccountSecurityViewModel.kt 87.50% 1 Missing and 1 partial ⚠️
...om/bitwarden/network/service/DevicesServiceImpl.kt 0.00% 2 Missing ⚠️
...tings/accountsecurity/AccountSecurityNavigation.kt 0.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #6754      +/-   ##
==========================================
- Coverage   85.57%   83.50%   -2.07%     
==========================================
  Files         830      946     +116     
  Lines       58652    61284    +2632     
  Branches     8570     8682     +112     
==========================================
+ Hits        50189    51173     +984     
- Misses       5492     7111    +1619     
- Partials     2971     3000      +29     
Flag Coverage Δ
app-data 17.43% <3.80%> (-0.06%) ⬇️
app-ui-auth-tools 20.19% <0.00%> (-0.19%) ⬇️
app-ui-platform 15.95% <85.59%> (+0.69%) ⬆️
app-ui-vault 25.77% <0.00%> (-0.24%) ⬇️
authenticator 6.49% <0.00%> (-0.06%) ⬇️
lib-core-network-bridge 4.25% <0.00%> (-0.05%) ⬇️
lib-data-ui 1.02% <0.00%> (-0.02%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

daysAgo < 7 -> BitwardenString.active_this_week
daysAgo < 14 -> BitwardenString.active_last_week
daysAgo < 30 -> BitwardenString.active_this_month
else -> BitwardenString.active_over_thirty_days_ago
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So this is all based on number of days in a week/month. Should it be based on the day of the week?

If today is Monday, 3 days ago is last week, not this week, right?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have mirrored the other clients logic but that makes sense to me 🤔

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have updated the texts so that it is clearer 🙌

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The language does match the functionality now. But don't we want it to say this week, not last seven days?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was decided to keep it simpler as this change will affect all other clients. So we are keeping the logic and adapting the texts.

* requiring rounding (unlike the JavaScript equivalent).
*/
@Suppress("MagicNumber")
fun Instant?.toLastActivityLabel(clock: Clock, resourceManager: ResourceManager): String? {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this is a UI layer component, can we just return a Text?

24 -> DeviceTypeEntry(BitwardenString.cli, "MacOs")
25 -> DeviceTypeEntry(BitwardenString.cli, "Linux")
26 -> DeviceTypeEntry(BitwardenString.extension, "DuckDuckGo")
else -> return resourceManager.getString(BitwardenString.unknown_device)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same thing here, can we just return a Text

19 -> DeviceTypeEntry(BitwardenString.extension, "Vivaldi")
20 -> DeviceTypeEntry(BitwardenString.extension, "Safari")
21 -> DeviceTypeEntry(BitwardenString.sdk, "")
22 -> DeviceTypeEntry(BitwardenString.server, "")
Copy link
Copy Markdown
Collaborator

@david-livefront david-livefront Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should these say something?

Also, should these platforms be translatable?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Other clients are returning the same data.
Regarding translations, nothing is translatable on other clients 🤔

It makes sense to me to be able to translate words like "server" or "extension"


ManageDevicesState.ViewState.Error -> BitwardenErrorContent(
message = stringResource(
id = BitwardenString.generic_error_message,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have nothing more specific than this?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do not, mobile was based roughly on existing Pending Auth screen and Manage Devices extension screen.
Probably will see some more design work before being approved

)
}

else -> SessionItem(
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we make this exhaustive

Change return type from ui extensions from String to Text
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 2, 2026

Bitwarden Claude Code Review

Overall Assessment: REQUEST CHANGES

This PR adds a new device management screen behind a feature flag (ManageDevices). The change spans the network layer (new API endpoints for fetching devices), repository layer (new result types and mapping), and UI layer (new screen with ViewModel, navigation wiring, and feature flag gating on the Account Security screen). The implementation follows established codebase patterns well, including proper UDF architecture, result type handling, and test coverage for the new ViewModel, Screen, and repository methods.

Code Review Details
  • ⚠️ IMPORTANT: hideBottomSheet computed property uses AND logic instead of OR, preventing the notification bottom sheet from being dismissed on F-Droid builds and pre-TIRAMISU devices
    • ManageDevicesViewModel.kt:313-316

daysAgo < 30 -> BitwardenString.past_thirty_days
else -> BitwardenString.over_thirty_days_ago
}
return resourceManager.getString(resId).asText()
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that you are returning a Text you do not need the ResourceManager here anymore.

* Returns e.g. "Mobile - Android", "Extension - Chrome", "Desktop - Windows".
*/
@Suppress("CyclomaticComplexMethod", "MagicNumber")
fun Int.toReadableDeviceTypeName(resourceManager: ResourceManager): Text {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here, the ResourceManager is unnecessary

# Conflicts:
#	core/src/main/kotlin/com/bitwarden/core/data/manager/model/FlagKey.kt
Comment on lines +313 to +316
val hideBottomSheet: Boolean
get() = internalHideBottomSheet &&
!isFdroid &&
isBuildVersionAtLeast(Build.VERSION_CODES.TIRAMISU)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ IMPORTANT: Boolean logic is inverted -- on F-Droid builds (and pre-TIRAMISU), hideBottomSheet always returns false, so the "Skip for now" button cannot dismiss the notification bottom sheet.

Details and fix

The current AND logic requires all three conditions to be true simultaneously. When isFdroid is true, the expression !isFdroid evaluates to false, making the entire result false regardless of internalHideBottomSheet. This means:

  1. On F-Droid with API 33+: the bottom sheet shows (asking to enable push notifications that don't work on F-Droid) and cannot be dismissed.
  2. On pre-TIRAMISU: same undismissable state, though the permission check in the Screen may mask this in some cases.

The fix is to use OR logic so that any single reason to hide the sheet is sufficient:

Suggested change
val hideBottomSheet: Boolean
get() = internalHideBottomSheet &&
!isFdroid &&
isBuildVersionAtLeast(Build.VERSION_CODES.TIRAMISU)
val hideBottomSheet: Boolean
get() = internalHideBottomSheet ||
isFdroid ||
!isBuildVersionAtLeast(Build.VERSION_CODES.TIRAMISU)

The @ChecksSdkIntAtLeast annotation on this property should also be removed since the corrected logic no longer solely checks for a minimum SDK level.

return if (entry.platform.isNotEmpty()) {
entry.categoryResId.asText()
.concat(
" - ${entry.platform}".asText(),
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of concat, can we make these format args?

<string name="continue_without_syncing">Continue without syncing</string>
<string name="external_link">External link</string>
<string name="external_link_format" comment="Used for accessibility to indicate that tapping this item will leave the app">%1$s, External link</string>
<string name="manage_devices">Manage devices</string>
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This managed devices string is being used in both prod code and for feature flags.

The feature flag name should be in the unlocalized file.

}

private fun handleOnLifecycleResumed() {
updateAuthRequestList()
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The updateAuthRequestList is a flow, why reset it on resume?

authRequestsLoaded = false,
)
}
updateAuthRequestList()
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here, why restart the flow here?

private fun fetchAllDevices() {
devicesJob.cancel()
devicesJob = viewModelScope.launch {
coroutineScope {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the purpose of the coroutineScope here?

@Parcelize
data class ManageDevicesState(
val authRequests: List<AuthRequest>,
val devices: List<DeviceInfo>,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we make all these lists into ImmuableLists

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai-review-vnext Request a Claude code review using the vNext workflow app:authenticator Bitwarden Authenticator app context app:password-manager Bitwarden Password Manager app context innovation Feature work related to Innovation Sprint or BEEEP projects t:feature Change Type - Feature Development

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants