Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 111 additions & 0 deletions BitwardenShared/Core/Auth/Models/Domain/DeviceListItem.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import BitwardenKit
import BitwardenResources
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.

BitwardenResources is imported but not used in this file. Consider removing it to avoid unused-import warnings.

Suggested change
import BitwardenResources

Copilot uses AI. Check for mistakes.
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
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.

displayName is currently set to type.displayName, ignoring the server-provided device.name. This will make all list entries show only the derived type/category string rather than the user/device name. Consider using device.name when present (with a fallback to type.displayName).

Suggested change
displayName = type.displayName
displayName = device.name?.isEmpty == false ? device.name! : type.displayName

Copilot uses AI. Check for mistakes.
deviceType = type
isTrusted = device.isTrusted
isCurrentSession = false
hasPendingRequest = false
activityStatus = DeviceActivityStatus(from: device.lastActivityDate, timeProvider: timeProvider)
firstLogin = device.creationDate
lastActivityDate = device.lastActivityDate
pendingRequest = nil
}
}
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
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.today, thisWeek, lastWeek, thisMonth, overThirtyDaysAgo, and unknown don’t appear to be backed by keys in BitwardenResources/Localizations/en.lproj/Localizable.strings, so SwiftGen likely won’t generate these properties and this will not compile. Add the missing localization keys (at least in en.lproj) or change the code to use existing keys.

Suggested change
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"

Copilot uses AI. Check for mistakes.
}
}
}
147 changes: 147 additions & 0 deletions BitwardenShared/Core/Auth/Models/Enum/DeviceType.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import BitwardenKit
import BitwardenResources
import Foundation
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.

Foundation is imported but not used in this file. Consider removing it to avoid unused-import warnings.

Suggested change
import Foundation

Copilot uses AI. Check for mistakes.

// MARK: - DeviceTypeCategory

/// The category of a device type.
///
enum DeviceTypeCategory: String, Sendable {
case mobile
case `extension`
case webApp
case desktop
case cli
case sdk
case server

/// The localized display name for the category.
var displayName: String {
switch self {
case .mobile:
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
Comment on lines +22 to +34
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.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.

Suggested change
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 uses AI. Check for mistakes.
}
}
}

// MARK: - DeviceType Extension

extension DeviceType {
// MARK: Known Device Type Values

static let android: DeviceType = 0
static let iOS: DeviceType = 1
static let chromeExtension: DeviceType = 2
static let firefoxExtension: DeviceType = 3
static let operaExtension: DeviceType = 4
static let edgeExtension: DeviceType = 5
static let windowsDesktop: DeviceType = 6
static let macOsDesktop: DeviceType = 7
static let linuxDesktop: DeviceType = 8
static let chromeBrowser: DeviceType = 9
static let firefoxBrowser: DeviceType = 10
static let operaBrowser: DeviceType = 11
static let edgeBrowser: DeviceType = 12
static let ieBrowser: DeviceType = 13
static let unknownBrowser: DeviceType = 14
static let androidAmazon: DeviceType = 15
static let uwp: DeviceType = 16
static let safariBrowser: DeviceType = 17
static let vivaldiBrowser: DeviceType = 18
static let vivaldiExtension: DeviceType = 19
static let safariExtension: DeviceType = 20
static let sdk: DeviceType = 21
static let server: DeviceType = 22
static let windowsCLI: DeviceType = 23
static let macOsCLI: DeviceType = 24
static let linuxCLI: DeviceType = 25
static let duckDuckGoBrowser: DeviceType = 26

// MARK: Properties

/// The category of the device type.
var category: DeviceTypeCategory {
switch self {
case Self.android, Self.androidAmazon, Self.iOS:
.mobile
case Self.chromeExtension, Self.edgeExtension, Self.firefoxExtension, Self.operaExtension,
Self.safariExtension, Self.vivaldiExtension:
.extension
case Self.chromeBrowser, Self.duckDuckGoBrowser, Self.edgeBrowser, Self.firefoxBrowser,
Self.ieBrowser, Self.operaBrowser, Self.safariBrowser, Self.unknownBrowser, Self.vivaldiBrowser:
.webApp
case Self.linuxDesktop, Self.macOsDesktop, Self.uwp, Self.windowsDesktop:
.desktop
case Self.linuxCLI, Self.macOsCLI, Self.windowsCLI:
.cli
case Self.sdk:
.sdk
case Self.server:
.server
default:
.mobile
}
}

/// The platform name for the device type.
var platform: String {
switch self {
case Self.android:
"Android"
case Self.iOS:
"iOS"
case Self.androidAmazon:
"Amazon"
case Self.chromeBrowser, Self.chromeExtension:
"Chrome"
case Self.firefoxBrowser, Self.firefoxExtension:
"Firefox"
case Self.operaBrowser, Self.operaExtension:
"Opera"
case Self.edgeBrowser, Self.edgeExtension:
"Edge"
case Self.vivaldiBrowser, Self.vivaldiExtension:
"Vivaldi"
case Self.safariBrowser, Self.safariExtension:
"Safari"
case Self.ieBrowser:
"IE"
case Self.duckDuckGoBrowser:
"DuckDuckGo"
case Self.unknownBrowser:
Localizations.unknown
case Self.windowsCLI, Self.windowsDesktop:
"Windows"
case Self.macOsCLI, Self.macOsDesktop:
"macOS"
case Self.linuxCLI, Self.linuxDesktop:
"Linux"
case Self.uwp:
"Windows UWP"
case Self.sdk, Self.server:
""
default:
Localizations.unknown
}
Comment on lines +123 to +137
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 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.

Copilot uses AI. Check for mistakes.
}

/// The display name for the device type, combining category and platform.
var displayName: String {
if platform.isEmpty {
return category.displayName
}
return "\(category.displayName) - \(platform)"
}
}
34 changes: 34 additions & 0 deletions BitwardenShared/Core/Auth/Models/Response/DeviceResponse.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import BitwardenKit
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.

BitwardenKit is imported but not used in this file. If warnings are treated as errors in CI, this will fail the build; otherwise it still adds noise. Remove the unused import.

Suggested change
import BitwardenKit

Copilot uses AI. Check for mistakes.
import Foundation
import Networking

// MARK: - DeviceResponse

/// A data structure representing a device response from the API.
///
public struct DeviceResponse: JSONResponse, Equatable, Sendable, Identifiable, Hashable {
public static let decoder = JSONDecoder.defaultDecoder

// MARK: Properties

/// The unique identifier of the device.
public let id: String

/// The name of the device.
let name: String?

/// The unique identifier for this device instance.
let identifier: String

/// The numeric type of the device (maps to DeviceType).
let type: Int

/// The date the device was first registered.
let creationDate: Date

/// Whether the device is trusted.
let isTrusted: Bool

/// The date of the last activity on this device.
let lastActivityDate: Date?
}
Comment on lines +9 to +34
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.

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.

Copilot uses AI. Check for mistakes.
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
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.

DevicesListResponse 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 decoding the nested [DeviceResponse]) would help catch API contract mismatches early.

Copilot uses AI. Check for mistakes.
Loading
Loading