Skip to content

[PM-33981] feat: Add device management models and API layer#2870

Open
andrebispo5 wants to merge 3 commits into
mainfrom
pm-33981/device-table-core
Open

[PM-33981] feat: Add device management models and API layer#2870
andrebispo5 wants to merge 3 commits into
mainfrom
pm-33981/device-table-core

Conversation

@andrebispo5

Copy link
Copy Markdown
Contributor

🎟️ Tracking

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

📔 Objective

Adds the complete data and API layer for the device management feature — no UI changes in this PR.

  • Promotes DeviceType from an Int typealias to a proper enum DeviceType: Int in BitwardenKit, with a safe Decodable fallback to .unknownBrowser for unknown server values
  • Adds DeviceTypeCategory and DeviceType display extensions (category, platform, displayName, pendingRequestMatchKey) in BitwardenShared Core
  • Adds DeviceActivityStatus enum mapping server timestamps to activity buckets using calendar-day comparison
  • Adds DeviceListItem domain model, DeviceResponse/DevicesListResponse API models, CurrentDeviceRequest/DevicesListRequest, and DeviceAPIService with getCurrentDevice(appId:) and getDevices() endpoints
  • Promotes deviceAPIService to a proper ServiceContainer init parameter (consistent with all other services); updates withMocks() default to MockDeviceAPIService()
  • Adds unit tests for all new types and request/response models; adds JSON fixtures

📸 Screenshots

No UI changes in this PR.

- Promote DeviceType from Int typealias to a proper enum in BitwardenKit
  with a safe Decodable fallback to .unknownBrowser for unknown values;
  update DefaultHeadersRequestHandler and IdentityTokenRequestModel to
  use .rawValue where an Int is required
- Add DeviceTypeCategory and DeviceType display extensions (category,
  platform, displayName, pendingRequestMatchKey) in BitwardenShared Core
- Add DeviceActivityStatus enum mapping server dates to activity buckets
  using calendar-day comparison
- Add DeviceListItem domain model, DeviceResponse/DevicesListResponse API
  models, CurrentDeviceRequest/DevicesListRequest, and DeviceAPIService
  with getCurrentDevice(appId:) and getDevices() methods
- Add deviceAPIService as a proper init parameter to ServiceContainer
  (consistent with all other services); update withMocks() default to
  MockDeviceAPIService()
- Add tests for all new types and request/response models; add JSON
  fixtures CurrentDevice.json and DevicesList.json
Copilot AI review requested due to automatic review settings July 9, 2026 09:06
@andrebispo5 andrebispo5 requested review from a team and matt-livefront as code owners July 9, 2026 09:06
@github-actions github-actions Bot added app:password-manager Bitwarden Password Manager app context app:authenticator Bitwarden Authenticator app context t:feature labels Jul 9, 2026
@andrebispo5 andrebispo5 added the ai-review Request a Claude code review label Jul 9, 2026
@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

🤖 Bitwarden Claude Code Review

Overall Assessment: APPROVE

Reviewed the device management data and API layer: the DeviceType enum migration in BitwardenKit, DeviceTypeCategory/DeviceActivityStatus/DeviceListItem models, DeviceResponse/DevicesListResponse API models, the two new DeviceAPIService endpoints, and the ServiceContainer DI wiring. The change is well-tested (unit tests plus JSON fixtures cover success, failure, and unknown-value decode paths), uses the safe @DefaultValue fallback for forward-compatible decoding, and follows established DI and networking conventions.

Code Review Details

No new findings. The DeviceType typealias→enum migration is consistent across all call sites (Constants.deviceType, DeviceInfo.type, header and token request models all correctly use .rawValue), and DeviceActivityStatus now uses calendar-month comparison to avoid the misclassification raised in an earlier review thread.

All existing review threads on this PR appear addressed in the current code (the DeviceListItem initializer is in an extension, DeviceType adopts DefaultValueProvider, the DeviceActivityStatus month boundary uses calendar granularity, and the ServiceContainer stored-property change is explained as required for withMocks(deviceAPIService:) injection).

@andrebispo5 andrebispo5 added the innovation Feature work related to Innovation Sprint or BEEEP projects label Jul 9, 2026

Copilot AI left a comment

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.

Pull request overview

Adds the device-management data/API layer (no UI), including new device request/response models, domain mapping helpers, and a promoted DeviceAPIService dependency in ServiceContainer.

Changes:

  • Introduces device API models/requests (DeviceResponse, DevicesListResponse, CurrentDeviceRequest, DevicesListRequest) and extends DeviceAPIService with getCurrentDevice(appId:) and getDevices().
  • Promotes DeviceType in BitwardenKit from an Int typealias to a real enum with safe decoding fallback; updates dependent request/header building to use rawValue.
  • Adds device display/mapping utilities (DeviceTypeCategory, DeviceActivityStatus, DeviceListItem) plus fixtures and unit tests.

Reviewed changes

Copilot reviewed 26 out of 26 changed files in this pull request and generated no comments.

Show a summary per file
File Description
BitwardenShared/Core/Platform/Services/TestHelpers/ServiceContainer+Mocks.swift Adds injectable deviceAPIService to mocks container defaults.
BitwardenShared/Core/Platform/Services/ServiceContainer.swift Promotes deviceAPIService to an initializer dependency and stores it instead of computing from apiService.
BitwardenShared/Core/Auth/Services/API/Device/Requests/DevicesListRequestTests.swift Adds Swift Testing coverage for DevicesListRequest defaults.
BitwardenShared/Core/Auth/Services/API/Device/Requests/DevicesListRequest.swift Adds GET /devices request model.
BitwardenShared/Core/Auth/Services/API/Device/Requests/CurrentDeviceRequestTests.swift Adds Swift Testing coverage for CurrentDeviceRequest defaults/path interpolation.
BitwardenShared/Core/Auth/Services/API/Device/Requests/CurrentDeviceRequest.swift Adds GET /devices/identifier/{appId} request model.
BitwardenShared/Core/Auth/Services/API/Device/Fixtures/DevicesList.json Adds JSON fixture for devices list response decoding/tests.
BitwardenShared/Core/Auth/Services/API/Device/Fixtures/CurrentDevice.json Adds JSON fixture for current device response decoding/tests.
BitwardenShared/Core/Auth/Services/API/Device/Fixtures/APITestData+Device.swift Registers device fixtures for API tests.
BitwardenShared/Core/Auth/Services/API/Device/DeviceAPIServiceTests.swift Adds unit tests for new DeviceAPIService endpoints.
BitwardenShared/Core/Auth/Services/API/Device/DeviceAPIService.swift Extends protocol + APIService conformance with getCurrentDevice/getDevices.
BitwardenShared/Core/Auth/Models/Response/Fixtures/DeviceResponse+Fixtures.swift Adds test fixture builder for DeviceResponse.
BitwardenShared/Core/Auth/Models/Response/DevicesListResponseTests.swift Adds decoding test for DevicesListResponse.
BitwardenShared/Core/Auth/Models/Response/DevicesListResponse.swift Adds response wrapper model for list endpoint.
BitwardenShared/Core/Auth/Models/Response/DeviceResponseTests.swift Adds decoding test for DeviceResponse.
BitwardenShared/Core/Auth/Models/Response/DeviceResponse.swift Adds device API response model.
BitwardenShared/Core/Auth/Models/Request/IdentityTokenRequestModel.swift Updates device type form encoding to use DeviceType.rawValue.
BitwardenShared/Core/Auth/Models/Enum/DeviceType.swift Adds device type categorization + display utilities on BitwardenKit.DeviceType.
BitwardenShared/Core/Auth/Models/Enum/DeviceActivityStatusTests.swift Adds tests for activity bucketing and localization strings.
BitwardenShared/Core/Auth/Models/Enum/DeviceActivityStatus.swift Adds calendar-day-based bucketing for “last active” timestamps.
BitwardenShared/Core/Auth/Models/Domain/Fixtures/DeviceListItem+Fixtures.swift Adds test fixture builder for DeviceListItem.
BitwardenShared/Core/Auth/Models/Domain/DeviceListItem.swift Adds UI-friendly domain model + mapping init from DeviceResponse.
BitwardenResources/Localizations/en.lproj/Localizable.strings Adds localization keys for device category/status/display formatting.
BitwardenKit/Core/Platform/Utilities/Constants.swift Removes DeviceType typealias and updates constants to use new enum.
BitwardenKit/Core/Platform/Services/API/Handlers/DefaultHeadersRequestHandler.swift Updates device-type header value to use rawValue.
BitwardenKit/Core/Platform/Models/Enum/DeviceType.swift Introduces DeviceType: Int enum with safe decoding fallback.

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

@codecov

codecov Bot commented Jul 9, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 91.97861% with 15 lines in your changes missing coverage. Please review.
✅ Project coverage is 79.00%. Comparing base (85f3884) to head (b8bd668).
⚠️ Report is 5 commits behind head on main.

Files with missing lines Patch % Lines
...hared/Core/Auth/Models/Domain/DeviceListItem.swift 0.00% 13 Missing ⚠️
...d/Core/Auth/Models/Enum/DeviceActivityStatus.swift 96.29% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2870      +/-   ##
==========================================
- Coverage   81.27%   79.00%   -2.28%     
==========================================
  Files        1027     1157     +130     
  Lines       66109    73641    +7532     
==========================================
+ Hits        53733    58183    +4450     
- Misses      12376    15458    +3082     

☔ View full report in Codecov by Harness.
📢 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.

…or backward compat

Tests that configure device API responses via MockHTTPClient (e.g.
LoginProcessorTests) continue to work unchanged. Callers that need
precise mock control pass MockDeviceAPIService explicitly.
Comment thread BitwardenKit/Core/Platform/Models/Enum/DeviceType.swift Outdated
Comment on lines +49 to +107
/// Initializes a `DeviceListItem` with all properties.
///
/// - Parameters:
/// - activityStatus: The activity status of the device.
/// - deviceType: The type of the device.
/// - displayName: The display name of the device.
/// - firstLogin: The date of the first login on this device.
/// - id: The server-assigned UUID for this device record.
/// - identifier: The client-generated UUID identifying the app installation.
/// - isCurrentSession: Whether this is the current session's device.
/// - isTrusted: Whether the device is trusted.
/// - lastActivityDate: The date of the last activity on this device.
/// - pendingRequest: The most recent pending login request for this device.
///
init(
activityStatus: DeviceActivityStatus,
deviceType: DeviceType,
displayName: String,
firstLogin: Date,
id: String,
identifier: String,
isCurrentSession: Bool,
isTrusted: Bool,
lastActivityDate: Date?,
pendingRequest: LoginRequest?,
) {
self.activityStatus = activityStatus
self.deviceType = deviceType
self.displayName = displayName
self.firstLogin = firstLogin
self.id = id
self.identifier = identifier
self.isCurrentSession = isCurrentSession
self.isTrusted = isTrusted
self.lastActivityDate = lastActivityDate
self.pendingRequest = pendingRequest
}

/// Initializes a `DeviceListItem` from a `DeviceResponse`.
///
/// - Parameters:
/// - device: The device response from the API.
/// - timeProvider: The time provider to use for calculating the activity status.
///
init(
device: DeviceResponse,
timeProvider: TimeProvider,
) {
activityStatus = DeviceActivityStatus(from: device.lastActivityDate, timeProvider: timeProvider)
deviceType = device.type
displayName = device.type.displayName
firstLogin = device.creationDate
id = device.id
identifier = device.identifier
isCurrentSession = false
isTrusted = device.isTrusted
lastActivityDate = device.lastActivityDate
pendingRequest = nil
}

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.

🎨 If you move the custom initializer init(device:timeProvider) into an extension on this type, then you'll get the top initializer synthesized for free.

Comment on lines +87 to +88
case 15 ... 30:
self = .thisMonth

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.

🤔 Shouldn't "this month" only be the case if the month components are the same? If it's July 15, and the device was last active June 20 (25 days ago), this would return .thisMonth, but that's actually last month.

Comment on lines -1277 to -1279
var deviceAPIService: DeviceAPIService {
apiService
}

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.

❓ I'm curious why change this? Do we need to store a copy of the APIService as DeviceAPIService in the service container instead of just using this computed variable?

@andrebispo5 andrebispo5 Jul 13, 2026

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's needed as a stored property, not revertible to a computed passthrough — checked the git history on this branch and it was a deliberate change. Earlier revisions had a test-only mutable deviceAPIServiceOverride var with a computed deviceAPIService property (deviceAPIServiceOverride ?? apiService), which is the anti-pattern this commit removed. Promoting it to a proper init parameter lets ServiceContainer.withMocks(deviceAPIService:) inject a real MockDeviceAPIService through the standard DI path (consistent with authService, configService, and most other services in the container), instead of a mutable side-door override. Reverting to a computed { apiService } passthrough would mean bringing that mutable override var back to keep it testable — happy to do that if you'd prefer it, but I think the stored-property version is the better outcome here.

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.

⛏️ Should this be named DeviceTypeCategory.swift to match the enum?

Comment on lines +42 to +46
public init(from decoder: any Decoder) throws {
let container = try decoder.singleValueContainer()
let rawValue = try container.decode(Int.self)
self = DeviceType(rawValue: rawValue) ?? .unknownBrowser
}

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 implementing a custom initializer to provide a default unknown value, you can use the DefaultValue property wrapper.

DeviceType would need to conform to DefaultValueProvider:

// MARK: - DefaultValueProvider

extension DeviceType: DefaultValueProvider {
    public static var defaultValue: DeviceType { .unknownBrowser }
}

And then in DeviceResponse, the type can be wrapped with the property wrapper:

    @DefaultValue var type: DeviceType

- Use the DefaultValue property wrapper for DeviceResponse.type instead
  of a hand-rolled Decodable init, matching the CollectionType/
  CipherRepromptType pattern; add Sendable conformance to DefaultValue
  so it composes with Sendable response models
- Fix DeviceActivityStatus to classify "this month" by actual calendar
  month instead of a fixed 15-30 day range, which mislabeled dates from
  a previous calendar month (e.g. June 20 relative to a July 15 "now")
- Move DeviceListItem's DeviceResponse-based init into an extension so
  the compiler synthesizes the memberwise init instead of hand-writing it
- Rename BitwardenShared DeviceType.swift to DeviceTypeCategory.swift to
  match the DeviceTypeCategory enum it defines
- Add missing coverage: DeviceType/DeviceResponse unknown-value decode
  fallback, and DeviceTypeCategory/DeviceType category-platform-
  displayName-pendingRequestMatchKey mappings
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai-review Request a Claude code review 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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants