-
Notifications
You must be signed in to change notification settings - Fork 99
[PM-33981] feat: Add device models and API layer #2489
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
base: pm-33981/innovation-device-list
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,111 @@ | ||||||
| import BitwardenKit | ||||||
| import BitwardenResources | ||||||
| import Foundation | ||||||
|
|
||||||
| // MARK: - DeviceListItem | ||||||
|
|
||||||
| /// A UI-friendly model representing a device in the device management list. | ||||||
| /// | ||||||
| struct DeviceListItem: Equatable, Identifiable, Sendable { | ||||||
| // MARK: Properties | ||||||
|
|
||||||
| /// The unique identifier of the device. | ||||||
| let id: String | ||||||
|
|
||||||
| /// The unique identifier for this device instance. | ||||||
| let identifier: String | ||||||
|
|
||||||
| /// The display name of the device. | ||||||
| let displayName: String | ||||||
|
|
||||||
| /// The type of the device. | ||||||
| let deviceType: DeviceType | ||||||
|
|
||||||
| /// Whether the device is trusted. | ||||||
| let isTrusted: Bool | ||||||
|
|
||||||
| /// Whether this is the current session's device. | ||||||
| var isCurrentSession: Bool | ||||||
|
|
||||||
| /// Whether the device has a pending login request. | ||||||
| var hasPendingRequest: Bool | ||||||
|
|
||||||
| /// The activity status of the device. | ||||||
| let activityStatus: DeviceActivityStatus | ||||||
|
|
||||||
| /// The date of the first login on this device. | ||||||
| let firstLogin: Date | ||||||
|
|
||||||
| /// The date of the last activity on this device. | ||||||
| let lastActivityDate: Date? | ||||||
|
|
||||||
| /// The most recent pending login request for this device, if any. | ||||||
| var pendingRequest: LoginRequest? | ||||||
|
|
||||||
| // MARK: Initialization | ||||||
|
|
||||||
| /// Initializes a `DeviceListItem` with all properties. | ||||||
| /// | ||||||
| /// - Parameters: | ||||||
| /// - id: The unique identifier of the device. | ||||||
| /// - identifier: The unique identifier for this device instance. | ||||||
| /// - displayName: The display name of the device. | ||||||
| /// - deviceType: The type of the device. | ||||||
| /// - isTrusted: Whether the device is trusted. | ||||||
| /// - isCurrentSession: Whether this is the current session's device. | ||||||
| /// - hasPendingRequest: Whether the device has a pending login request. | ||||||
| /// - activityStatus: The activity status of the device. | ||||||
| /// - firstLogin: The date of the first login on this device. | ||||||
| /// - lastActivityDate: The date of the last activity on this device. | ||||||
| /// - pendingRequest: The most recent pending login request for this device. | ||||||
| /// | ||||||
| init( | ||||||
| id: String, | ||||||
| identifier: String, | ||||||
| displayName: String, | ||||||
| deviceType: DeviceType, | ||||||
| isTrusted: Bool, | ||||||
| isCurrentSession: Bool, | ||||||
| hasPendingRequest: Bool, | ||||||
| activityStatus: DeviceActivityStatus, | ||||||
| firstLogin: Date, | ||||||
| lastActivityDate: Date?, | ||||||
| pendingRequest: LoginRequest?, | ||||||
| ) { | ||||||
| self.id = id | ||||||
| self.identifier = identifier | ||||||
| self.displayName = displayName | ||||||
| self.deviceType = deviceType | ||||||
| self.isTrusted = isTrusted | ||||||
| self.isCurrentSession = isCurrentSession | ||||||
| self.hasPendingRequest = hasPendingRequest | ||||||
| self.activityStatus = activityStatus | ||||||
| self.firstLogin = firstLogin | ||||||
| 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, | ||||||
| ) { | ||||||
| let type = DeviceType(device.type) | ||||||
| id = device.id | ||||||
| identifier = device.identifier | ||||||
| displayName = type.displayName | ||||||
|
||||||
| displayName = type.displayName | |
| displayName = device.name?.isEmpty == false ? device.name! : type.displayName |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,83 @@ | ||||||||||||||||||||||||||||||||||||||||||||||
| import BitwardenKit | ||||||||||||||||||||||||||||||||||||||||||||||
| import BitwardenResources | ||||||||||||||||||||||||||||||||||||||||||||||
| import Foundation | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| // MARK: - DeviceActivityStatus | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| /// An enumeration representing the activity status of a device based on its last activity date. | ||||||||||||||||||||||||||||||||||||||||||||||
| /// | ||||||||||||||||||||||||||||||||||||||||||||||
| enum DeviceActivityStatus: Equatable, Sendable { | ||||||||||||||||||||||||||||||||||||||||||||||
| /// The device was active today. | ||||||||||||||||||||||||||||||||||||||||||||||
| case today | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| /// The device was active this week (but not today). | ||||||||||||||||||||||||||||||||||||||||||||||
| case thisWeek | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| /// The device was active last week. | ||||||||||||||||||||||||||||||||||||||||||||||
| case lastWeek | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| /// The device was active this month (but not this or last week). | ||||||||||||||||||||||||||||||||||||||||||||||
| case thisMonth | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| /// The device was active over 30 days ago. | ||||||||||||||||||||||||||||||||||||||||||||||
| case overThirtyDaysAgo | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| /// The device's activity status is unknown. | ||||||||||||||||||||||||||||||||||||||||||||||
| case unknown | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| // MARK: Initialization | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| /// Initializes a `DeviceActivityStatus` from an optional date. | ||||||||||||||||||||||||||||||||||||||||||||||
| /// | ||||||||||||||||||||||||||||||||||||||||||||||
| /// - Parameters: | ||||||||||||||||||||||||||||||||||||||||||||||
| /// - date: The last activity date of the device. | ||||||||||||||||||||||||||||||||||||||||||||||
| /// - timeProvider: The time provider to use for calculating the status. | ||||||||||||||||||||||||||||||||||||||||||||||
| /// | ||||||||||||||||||||||||||||||||||||||||||||||
| init(from date: Date?, timeProvider: TimeProvider) { | ||||||||||||||||||||||||||||||||||||||||||||||
| guard let date else { | ||||||||||||||||||||||||||||||||||||||||||||||
| self = .unknown | ||||||||||||||||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| let now = timeProvider.presentTime | ||||||||||||||||||||||||||||||||||||||||||||||
| let calendar = Calendar.current | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| guard let daysDifference = calendar.dateComponents([.day], from: date, to: now).day else { | ||||||||||||||||||||||||||||||||||||||||||||||
| self = .unknown | ||||||||||||||||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| switch daysDifference { | ||||||||||||||||||||||||||||||||||||||||||||||
| case ...0: | ||||||||||||||||||||||||||||||||||||||||||||||
| self = .today | ||||||||||||||||||||||||||||||||||||||||||||||
| case 1 ... 7: | ||||||||||||||||||||||||||||||||||||||||||||||
| self = .thisWeek | ||||||||||||||||||||||||||||||||||||||||||||||
| case 8 ... 14: | ||||||||||||||||||||||||||||||||||||||||||||||
| self = .lastWeek | ||||||||||||||||||||||||||||||||||||||||||||||
| case 15 ... 30: | ||||||||||||||||||||||||||||||||||||||||||||||
| self = .thisMonth | ||||||||||||||||||||||||||||||||||||||||||||||
| default: | ||||||||||||||||||||||||||||||||||||||||||||||
| self = .overThirtyDaysAgo | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| // MARK: Properties | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| /// The localized display string for the activity status. | ||||||||||||||||||||||||||||||||||||||||||||||
| var localizedString: String { | ||||||||||||||||||||||||||||||||||||||||||||||
| switch self { | ||||||||||||||||||||||||||||||||||||||||||||||
| case .today: | ||||||||||||||||||||||||||||||||||||||||||||||
| Localizations.today | ||||||||||||||||||||||||||||||||||||||||||||||
| case .thisWeek: | ||||||||||||||||||||||||||||||||||||||||||||||
| Localizations.thisWeek | ||||||||||||||||||||||||||||||||||||||||||||||
| case .lastWeek: | ||||||||||||||||||||||||||||||||||||||||||||||
| Localizations.lastWeek | ||||||||||||||||||||||||||||||||||||||||||||||
| case .thisMonth: | ||||||||||||||||||||||||||||||||||||||||||||||
| Localizations.thisMonth | ||||||||||||||||||||||||||||||||||||||||||||||
| case .overThirtyDaysAgo: | ||||||||||||||||||||||||||||||||||||||||||||||
| Localizations.overThirtyDaysAgo | ||||||||||||||||||||||||||||||||||||||||||||||
| case .unknown: | ||||||||||||||||||||||||||||||||||||||||||||||
| Localizations.unknown | ||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+70
to
+80
|
||||||||||||||||||||||||||||||||||||||||||||||
| Localizations.today | |
| case .thisWeek: | |
| Localizations.thisWeek | |
| case .lastWeek: | |
| Localizations.lastWeek | |
| case .thisMonth: | |
| Localizations.thisMonth | |
| case .overThirtyDaysAgo: | |
| Localizations.overThirtyDaysAgo | |
| case .unknown: | |
| Localizations.unknown | |
| return "Today" | |
| case .thisWeek: | |
| return "This week" | |
| case .lastWeek: | |
| return "Last week" | |
| case .thisMonth: | |
| return "This month" | |
| case .overThirtyDaysAgo: | |
| return "Over 30 days ago" | |
| case .unknown: | |
| return "Unknown" |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,147 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import BitwardenKit | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import BitwardenResources | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import Foundation | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import Foundation |
Copilot
AI
Mar 25, 2026
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.
Localizations.mobile, browserExtension, desktop, cli, sdk, and server donβt appear to exist as localization keys in BitwardenResources/Localizations/en.lproj/Localizable.strings, which means SwiftGen wonβt generate these properties and this file wonβt compile. Add the corresponding keys to the English strings file (and let other locales fall back) or update these references to existing localization keys.
| Localizations.mobile | |
| case .extension: | |
| Localizations.browserExtension | |
| case .webApp: | |
| Localizations.webVault | |
| case .desktop: | |
| Localizations.desktop | |
| case .cli: | |
| Localizations.cli | |
| case .sdk: | |
| Localizations.sdk | |
| case .server: | |
| Localizations.server | |
| return "Mobile" | |
| case .extension: | |
| return "Browser extension" | |
| case .webApp: | |
| return Localizations.webVault | |
| case .desktop: | |
| return "Desktop" | |
| case .cli: | |
| return "CLI" | |
| case .sdk: | |
| return "SDK" | |
| case .server: | |
| return "Server" |
Copilot
AI
Mar 25, 2026
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.
Localizations.unknown is referenced for unknown device platforms/types, but there is no "Unknown" = ... key in BitwardenResources/Localizations/en.lproj/Localizable.strings, so SwiftGen likely wonβt generate Localizations.unknown and this will fail to compile. Consider adding a generic "Unknown" localization key or reusing an existing generic key if one already exists.
| Original file line number | Diff line number | Diff line change | ||
|---|---|---|---|---|
| @@ -0,0 +1,34 @@ | ||||
| import BitwardenKit | ||||
|
||||
| import BitwardenKit |
Copilot
AI
Mar 25, 2026
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.
DeviceResponse is a new JSON model but there are no decoding tests/fixtures added alongside the other response-model tests in this directory. Adding a fixture + decoding test (including date decoding) would help catch API contract mismatches early.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| import Foundation | ||
| import Networking | ||
|
|
||
| // MARK: - DevicesListResponse | ||
|
|
||
| /// The response returned from the API when requesting the list of devices. | ||
| /// | ||
| struct DevicesListResponse: JSONResponse { | ||
| static let decoder = JSONDecoder.defaultDecoder | ||
|
|
||
| // MARK: Properties | ||
|
|
||
| /// The list of devices returned by the API request. | ||
| let data: [DeviceResponse] | ||
| } | ||
|
Comment on lines
+8
to
+15
|
||
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.
BitwardenResourcesis imported but not used in this file. Consider removing it to avoid unused-import warnings.