Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
47 changes: 47 additions & 0 deletions BitwardenKit/Core/Platform/Models/Enum/DeviceType.swift
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 {
Comment thread
andrebispo5 marked this conversation as resolved.
Outdated
let container = try decoder.singleValueContainer()
let rawValue = try container.decode(Int.self)
self = DeviceType(rawValue: rawValue) ?? .unknownBrowser
}
Comment thread
matt-livefront marked this conversation as resolved.
Outdated
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public final class DefaultHeadersRequestHandler: RequestHandler {
public func handle(_ request: inout HTTPRequest) async throws -> HTTPRequest {
request.headers["Bitwarden-Client-Name"] = Constants.clientType
request.headers["Bitwarden-Client-Version"] = appVersion
request.headers["Device-Type"] = String(Constants.deviceType)
request.headers["Device-Type"] = String(Constants.deviceType.rawValue)
request.headers["User-Agent"] = userAgentBuilder.value

return request
Expand Down
7 changes: 2 additions & 5 deletions BitwardenKit/Core/Platform/Utilities/Constants.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,6 @@ import Foundation
/// A type alias for the client type.
public typealias ClientType = String

/// A type alias for the device type.
public typealias DeviceType = Int

// MARK: - Constants

/// Constant values reused throughout the app.
Expand All @@ -14,8 +11,8 @@ public enum Constants {
/// The client type corresponding to the app.
public static let clientType: ClientType = "mobile"

/// The device type, iOS = 1.
public static let deviceType: DeviceType = 1
/// The device type for this app.
public static let deviceType: DeviceType = .iOS

/// The number of days that a flight recorder log will remain on the device after the end date
/// before being automatically deleted.
Expand Down
15 changes: 15 additions & 0 deletions BitwardenResources/Localizations/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -1169,3 +1169,18 @@
"LineOfCredit" = "Line of credit";
"MoneyMarket" = "Money market";
"Savings" = "Savings";
"Today" = "Today";
"ThisWeek" = "This week";
"LastWeek" = "Last week";
"ThisMonth" = "This month";
"OverThirtyDaysAgo" = "Over 30 days ago";
"Unknown" = "Unknown";
/* Device display name. %1$@ = category (e.g. "Mobile"), %2$@ = platform (e.g. "iOS"). */
"DeviceDisplayName" = "%1$@ - %2$@";
"Mobile" = "Mobile";
"BrowserExtension" = "Browser extension";
"WebVaultDeviceType" = "Web vault";
"Desktop" = "Desktop";
"Cli" = "CLI";
"Sdk" = "SDK";
"Server" = "Server";
108 changes: 108 additions & 0 deletions BitwardenShared/Core/Auth/Models/Domain/DeviceListItem.swift
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
}
Comment thread
matt-livefront marked this conversation as resolved.
Outdated
}
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,
)
}
}
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

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.

default:
self = .overThirtyDaysAgo
}
}
}
Loading
Loading