-
Notifications
You must be signed in to change notification settings - Fork 137
[PM-33981] feat: Add device management models and API layer #2870
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
Open
andrebispo5
wants to merge
5
commits into
main
Choose a base branch
from
pm-33981/device-table-core
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 2 commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
94237b4
[PM-33981] feat: Add device management models and API layer
andrebispo5 59f9905
[PM-33981] fix: Default deviceAPIService to apiService in withMocks fβ¦
andrebispo5 b8bd668
[PM-33981] fix: Address PR review comments on device models
andrebispo5 57ef65f
[PM-33981] fix: Match DeviceActivityStatus day buckets to Android
andrebispo5 332105e
[PM-33981] fix: Match device activity strings and keys to Android worβ¦
andrebispo5 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| // MARK: - DeviceType | ||
|
|
||
| /// The type of device used to access the vault. | ||
| /// | ||
| public enum DeviceType: Int, Codable, Hashable, Sendable { | ||
| case android = 0 | ||
| case iOS = 1 | ||
| case chromeExtension = 2 | ||
| case firefoxExtension = 3 | ||
| case operaExtension = 4 | ||
| case edgeExtension = 5 | ||
| case windowsDesktop = 6 | ||
| case macOsDesktop = 7 | ||
| case linuxDesktop = 8 | ||
| case chromeBrowser = 9 | ||
| case firefoxBrowser = 10 | ||
| case operaBrowser = 11 | ||
| case edgeBrowser = 12 | ||
| case ieBrowser = 13 | ||
| case unknownBrowser = 14 | ||
| case androidAmazon = 15 | ||
| case uwp = 16 | ||
| case safariBrowser = 17 | ||
| case vivaldiBrowser = 18 | ||
| case vivaldiExtension = 19 | ||
| case safariExtension = 20 | ||
| case sdk = 21 | ||
| case server = 22 | ||
| case windowsCLI = 23 | ||
| case macOsCLI = 24 | ||
| case linuxCLI = 25 | ||
| case duckDuckGoBrowser = 26 | ||
|
|
||
| // MARK: Initialization | ||
|
|
||
| /// Initializes a `DeviceType` from a raw integer value, falling back to `.unknownBrowser` | ||
| /// for values not yet recognized by the client. | ||
| public init(_ rawValue: Int) { | ||
| self = DeviceType(rawValue: rawValue) ?? .unknownBrowser | ||
| } | ||
|
|
||
| public init(from decoder: any Decoder) throws { | ||
| let container = try decoder.singleValueContainer() | ||
| let rawValue = try container.decode(Int.self) | ||
| self = DeviceType(rawValue: rawValue) ?? .unknownBrowser | ||
| } | ||
|
matt-livefront marked this conversation as resolved.
Outdated
|
||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
108 changes: 108 additions & 0 deletions
108
BitwardenShared/Core/Auth/Models/Domain/DeviceListItem.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,108 @@ | ||
| import BitwardenKit | ||
| import Foundation | ||
|
|
||
| // MARK: - DeviceListItem | ||
|
|
||
| /// A UI-friendly model representing a device in the device management list. | ||
| /// | ||
| struct DeviceListItem: Equatable, Identifiable, Sendable { | ||
| // MARK: Properties | ||
|
|
||
| /// The activity status of the device. | ||
| let activityStatus: DeviceActivityStatus | ||
|
|
||
| /// The type of the device. | ||
| let deviceType: DeviceType | ||
|
|
||
| /// The display name of the device. | ||
| let displayName: String | ||
|
|
||
| /// The date of the first login on this device. | ||
| let firstLogin: Date | ||
|
|
||
| /// Whether the device has a pending login request. | ||
| /// | ||
| /// Computed from `pendingRequest` β `true` when a pending request is present. | ||
| var hasPendingRequest: Bool { pendingRequest != nil } | ||
|
|
||
| /// The server-assigned UUID that uniquely identifies this device record across all users. | ||
| let id: String | ||
|
|
||
| /// The client-generated UUID embedded in the app on this device, used for push notifications | ||
| /// and device recognition. | ||
| let identifier: String | ||
|
|
||
| /// Whether this is the current session's device. | ||
| var isCurrentSession: Bool | ||
|
|
||
| /// Whether the device is trusted. | ||
| let isTrusted: Bool | ||
|
|
||
| /// 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: | ||
| /// - 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 | ||
| } | ||
|
matt-livefront marked this conversation as resolved.
Outdated
|
||
| } | ||
32 changes: 32 additions & 0 deletions
32
BitwardenShared/Core/Auth/Models/Domain/Fixtures/DeviceListItem+Fixtures.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| import BitwardenKit | ||
| import Foundation | ||
|
|
||
| @testable import BitwardenShared | ||
|
|
||
| extension DeviceListItem { | ||
| static func fixture( | ||
| activityStatus: DeviceActivityStatus = .today, | ||
| deviceType: DeviceType = .iOS, | ||
| displayName: String = "Mobile - iOS", | ||
| firstLogin: Date = Date(timeIntervalSince1970: 1_704_067_200), | ||
| id: String = "device-id-1", | ||
| identifier: String = "device-identifier-1", | ||
| isCurrentSession: Bool = false, | ||
| isTrusted: Bool = true, | ||
| lastActivityDate: Date? = Date(timeIntervalSince1970: 1_718_452_200), | ||
| pendingRequest: LoginRequest? = nil, | ||
| ) -> DeviceListItem { | ||
| DeviceListItem( | ||
| activityStatus: activityStatus, | ||
| deviceType: deviceType, | ||
| displayName: displayName, | ||
| firstLogin: firstLogin, | ||
| id: id, | ||
| identifier: identifier, | ||
| isCurrentSession: isCurrentSession, | ||
| isTrusted: isTrusted, | ||
| lastActivityDate: lastActivityDate, | ||
| pendingRequest: pendingRequest, | ||
| ) | ||
| } | ||
| } |
93 changes: 93 additions & 0 deletions
93
BitwardenShared/Core/Auth/Models/Enum/DeviceActivityStatus.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,93 @@ | ||
| 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 last week. | ||
| case lastWeek | ||
|
|
||
| /// The device was active over 30 days ago. | ||
| case overThirtyDaysAgo | ||
|
|
||
| /// The device was active this month (but not this or last week). | ||
| case thisMonth | ||
|
|
||
| /// The device was active this week (but not today). | ||
| case thisWeek | ||
|
|
||
| /// The device was active today. | ||
| case today | ||
|
|
||
| /// The device's activity status is unknown. | ||
| case unknown | ||
|
|
||
| // MARK: Properties | ||
|
|
||
| /// The localized display string for the activity status. | ||
| var localizedString: String { | ||
| switch self { | ||
| case .lastWeek: | ||
| Localizations.lastWeek | ||
| case .overThirtyDaysAgo: | ||
| Localizations.overThirtyDaysAgo | ||
| case .thisMonth: | ||
| Localizations.thisMonth | ||
| case .thisWeek: | ||
| Localizations.thisWeek | ||
| case .today: | ||
| Localizations.today | ||
| case .unknown: | ||
| Localizations.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 | ||
|
|
||
| guard date <= now else { | ||
| self = .unknown | ||
| return | ||
| } | ||
|
|
||
| let calendar = Calendar.current | ||
| let startOfDate = calendar.startOfDay(for: date) | ||
| let startOfToday = calendar.startOfDay(for: now) | ||
|
|
||
| // `.day` is always non-nil when explicitly requested via `dateComponents([.day]:)`; | ||
| // the guard is defensive. | ||
| guard let daysDifference = calendar.dateComponents([.day], from: startOfDate, to: startOfToday).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 | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
| default: | ||
| self = .overThirtyDaysAgo | ||
| } | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.