Skip to content

[PM-33981] feat: Add device management UI components#2490

Draft
andrebispo5 wants to merge 1 commit intopm-33981/innovation-device-listfrom
pm-33981/device-management-ui
Draft

[PM-33981] feat: Add device management UI components#2490
andrebispo5 wants to merge 1 commit intopm-33981/innovation-device-listfrom
pm-33981/device-management-ui

Conversation

@andrebispo5
Copy link
Copy Markdown
Contributor

@andrebispo5 andrebispo5 commented Mar 25, 2026

Depends on: #2489

🎟️ Tracking

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

📔 Objective

Add the Device Management screen UI components including:

  • DeviceManagementView - Main view with device list and empty state
  • DeviceManagementProcessor - Handles loading devices, matching pending requests, and sorting
  • DeviceManagementState, DeviceManagementAction, DeviceManagementEffect - State management
  • DeviceRow - Individual device cell with status indicators

Copilot AI review requested due to automatic review settings March 25, 2026 17:21
@andrebispo5 andrebispo5 requested review from a team and matt-livefront as code owners March 25, 2026 17:21
@github-actions github-actions bot added app:password-manager Bitwarden Password Manager app context t:feature labels Mar 25, 2026
@andrebispo5 andrebispo5 marked this pull request as draft March 25, 2026 17:25
@github-actions
Copy link
Copy Markdown
Contributor

Logo
Checkmarx One – Scan Summary & Detailsc1ac878e-2039-46aa-bb15-2d81ed462aa1

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

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds the initial SwiftUI and state-management building blocks for a new Device Management screen under Account Security, including a list row UI and a processor to load/sort devices and associate pending login requests.

Changes:

  • Introduces DeviceManagementView with loading/empty/list states and pull-to-refresh.
  • Adds DeviceManagementProcessor + State/Action/Effect for loading devices, matching pending requests, and sorting.
  • Adds DeviceRow SwiftUI component with status badges and activity/first-login metadata.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 13 comments.

Show a summary per file
File Description
BitwardenShared/UI/Platform/Settings/Settings/AccountSecurity/DeviceManagement/DeviceRow.swift New device list cell UI with badges, metadata rows, and tap handling for pending requests.
BitwardenShared/UI/Platform/Settings/Settings/AccountSecurity/DeviceManagement/DeviceManagementView.swift New screen UI using LoadingView, empty state, and a list of DeviceRows.
BitwardenShared/UI/Platform/Settings/Settings/AccountSecurity/DeviceManagement/DeviceManagementState.swift New state container for loading state/toast/current device ID.
BitwardenShared/UI/Platform/Settings/Settings/AccountSecurity/DeviceManagement/DeviceManagementProcessor.swift New processor that fetches devices/current device/pending requests, matches/sorts results, and handles navigation/toast.
BitwardenShared/UI/Platform/Settings/Settings/AccountSecurity/DeviceManagement/DeviceManagementEffect.swift Defines .loadData effect for the processor.
BitwardenShared/UI/Platform/Settings/Settings/AccountSecurity/DeviceManagement/DeviceManagementAction.swift Defines user actions (tap, dismiss, toast lifecycle).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.


/// Formats a date for display with date and time.
private func formattedDateTime(_ date: Date?) -> String {
guard let date else { return Localizations.unknown }
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

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

Localizations.unknown doesn’t appear to exist in BitwardenResources/Localizations/en.lproj/Localizable.strings (no Unknown key). Add the missing localization key (and regenerate SwiftGen) or use an existing Localizations.* value (e.g., UnknownXErrorMessage is not a direct replacement).

Suggested change
guard let date else { return Localizations.unknown }
guard let date else {
return NSLocalizedString("Unknown", comment: "Fallback text for unknown date")
}

Copilot uses AI. Check for mistakes.
Comment on lines +139 to +145
/// Formats a date for display with date and time.
private func formattedDateTime(_ date: Date?) -> String {
guard let date else { return Localizations.unknown }
let formatter = DateFormatter()
formatter.dateStyle = .medium
formatter.timeStyle = .short
return formatter.string(from: date)
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

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

formattedDateTime creates a new DateFormatter every time it’s called, which is relatively expensive in a scrolling list. Consider using date.formatted(...) (as done elsewhere) or a cached/static formatter to avoid repeated allocations.

Suggested change
/// Formats a date for display with date and time.
private func formattedDateTime(_ date: Date?) -> String {
guard let date else { return Localizations.unknown }
let formatter = DateFormatter()
formatter.dateStyle = .medium
formatter.timeStyle = .short
return formatter.string(from: date)
/// A cached formatter for displaying dates with medium date and short time styles.
private static let dateTimeFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateStyle = .medium
formatter.timeStyle = .short
return formatter
}()
/// Formats a date for display with date and time.
private func formattedDateTime(_ date: Date?) -> String {
guard let date else { return Localizations.unknown }
return Self.dateTimeFormatter.string(from: date)

Copilot uses AI. Check for mistakes.
Comment on lines +25 to +33
HStack(alignment: .center) {
VStack(alignment: .leading, spacing: 4) {
VStack(alignment: .leading, spacing: 0) {
// Device name
Text(device.displayName)
.foregroundStyle(SharedAsset.Colors.textPrimary.swiftUIColor)
.styleGuide(.bodySemibold)
.accessibilityIdentifier("DeviceNameLabel")

Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

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

For VoiceOver, it’s typically better if the row reads as a single element (name + status + dates) instead of many separate labels. Consider applying .accessibilityElement(children: .combine) on the row’s main content container (similar to PendingRequestsView’s row implementation).

Copilot uses AI. Check for mistakes.
Comment on lines +110 to +114
deviceItems = matchPendingRequestsToDevices(deviceItems, pendingRequests: pendingRequests)

// Sort devices: current session first, then pending requests, then by activity.
deviceItems.sort { lhs, rhs in
// Current session always first.
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

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

This processor adds non-trivial logic (pending-request matching + multi-criteria sorting). Since similar flows (e.g. PendingRequestsProcessor) have unit tests, please add DeviceManagementProcessorTests covering matching/sorting and error handling to prevent regressions.

Copilot uses AI. Check for mistakes.
.accessibilityIdentifier("DeviceNameLabel")

if device.isTrusted {
Text(Localizations.trusted)
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

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

Localizations.trusted doesn’t appear to exist in BitwardenResources/Localizations/en.lproj/Localizable.strings (no Trusted key). Add the missing localization key (and regenerate SwiftGen) or update to an existing localization constant.

Suggested change
Text(Localizations.trusted)
Text("Trusted")

Copilot uses AI. Check for mistakes.
Spacer()

Image(asset: SharedAsset.Icons.chevronRight16)
.imageStyle(.accessoryIcon16)
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

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

The chevron is purely decorative/affordance; consider marking it as non-accessible (e.g., Image(decorative:) or .accessibilityHidden(true)) so VoiceOver doesn’t announce it separately from the row content.

Suggested change
.imageStyle(.accessoryIcon16)
.imageStyle(.accessoryIcon16)
.accessibilityHidden(true)

Copilot uses AI. Check for mistakes.
Comment on lines +17 to +25
var body: some View {
LoadingView(state: store.state.loadingState) { devices in
if devices.isEmpty {
empty
.scrollView(centerContentVertically: true)
} else {
devicesList(devices)
.scrollView()
}
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

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

There are snapshot tests for similar screens (e.g. PendingRequestsView+SnapshotTests.swift), but this new view doesn’t add any. Please consider adding snapshot coverage for the empty and populated states to catch UI regressions (including AX text size variants).

Copilot uses AI. Check for mistakes.
.scrollView()
}
}
.navigationBar(title: Localizations.manageDevices, titleDisplayMode: .inline)
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

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

Localizations.manageDevices doesn’t appear to exist in BitwardenResources/Localizations/en.lproj/Localizable.strings (no ManageDevices key). This will fail to compile unless the string is added (and SwiftGen updated).

Suggested change
.navigationBar(title: Localizations.manageDevices, titleDisplayMode: .inline)
.navigationBar(title: "Manage devices", titleDisplayMode: .inline)

Copilot uses AI. Check for mistakes.
.resizable()
.frame(width: 100, height: 100)

Text(Localizations.noDevicesFound)
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

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

Localizations.noDevicesFound doesn’t appear to exist in BitwardenResources/Localizations/en.lproj/Localizable.strings (no NoDevicesFound key). Add the missing localization key (and regenerate SwiftGen) to avoid build failures.

Suggested change
Text(Localizations.noDevicesFound)
Text("No devices found")

Copilot uses AI. Check for mistakes.
Comment on lines +113 to +117
Text(Localizations.firstLoginLabel)
.foregroundStyle(SharedAsset.Colors.textSecondary.swiftUIColor)
.styleGuide(.subheadlineSemibold)

Text(formattedDateTime(device.firstLogin))
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

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

Localizations.firstLoginLabel doesn’t appear to exist in BitwardenResources/Localizations/en.lproj/Localizable.strings (no FirstLoginLabel key). Add the missing localization key (and regenerate SwiftGen) or use an existing localization constant.

Copilot uses AI. Check for mistakes.
@andrebispo5 andrebispo5 added the innovation Feature work related to Innovation Sprint or BEEEP projects label Mar 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

app:password-manager Bitwarden Password Manager app context innovation Feature work related to Innovation Sprint or BEEEP projects t:feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants