Skip to content

Commit e853f2f

Browse files
[PM-19305] Enforce session timeout policy (#2127)
1 parent 336c94f commit e853f2f

29 files changed

+1275
-160
lines changed
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/// An object that represents a session timeout policy
2+
///
3+
public struct SessionTimeoutPolicy {
4+
// MARK: Properties
5+
6+
/// The action to perform on session timeout.
7+
public let timeoutAction: SessionTimeoutAction?
8+
9+
/// An enumeration of session timeout types to choose from.
10+
public let timeoutType: SessionTimeoutType?
11+
12+
/// An enumeration of session timeout values to choose from.
13+
public let timeoutValue: SessionTimeoutValue?
14+
15+
// MARK: Initialization
16+
17+
/// Initialize `SessionTimeoutPolicy` with the specified values.
18+
///
19+
/// - Parameters:
20+
/// - timeoutAction: The action to perform on session timeout.
21+
/// - timeoutType: The type of session timeout.
22+
/// - timeoutValue: The session timeout value.
23+
public init(
24+
timeoutAction: SessionTimeoutAction?,
25+
timeoutType: SessionTimeoutType?,
26+
timeoutValue: SessionTimeoutValue?,
27+
) {
28+
self.timeoutAction = timeoutAction
29+
self.timeoutType = timeoutType
30+
self.timeoutValue = timeoutValue
31+
}
32+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import BitwardenResources
2+
3+
/// The action to perform on session timeout.
4+
///
5+
public enum SessionTimeoutAction: Int, CaseIterable, Codable, Equatable, Menuable, Sendable {
6+
/// Lock the vault.
7+
case lock = 0
8+
9+
/// Log the user out.
10+
case logout = 1
11+
12+
/// All of the cases to show in the menu.
13+
public static let allCases: [SessionTimeoutAction] = [.lock, .logout]
14+
15+
public var localizedName: String {
16+
switch self {
17+
case .lock:
18+
Localizations.lock
19+
case .logout:
20+
Localizations.logOut
21+
}
22+
}
23+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
// MARK: - SessionTimeoutType
2+
3+
/// An enumeration of session timeout types to choose from.
4+
///
5+
public enum SessionTimeoutType: Codable, Equatable, Hashable, Sendable {
6+
/// Time out immediately.
7+
case immediately
8+
9+
/// Time out on app restart.
10+
case onAppRestart
11+
12+
/// Never time out the session.
13+
case never
14+
15+
/// A custom timeout value.
16+
case custom
17+
18+
// MARK: Properties
19+
20+
/// The string representation of a session timeout type.
21+
public var rawValue: String {
22+
switch self {
23+
case .immediately:
24+
"immediately"
25+
case .onAppRestart:
26+
"onAppRestart"
27+
case .never:
28+
"never"
29+
case .custom:
30+
"custom"
31+
}
32+
}
33+
34+
/// A safe string representation of the timeout type.
35+
public var timeoutType: String {
36+
switch self {
37+
case .immediately:
38+
"immediately"
39+
case .onAppRestart:
40+
"on app restart"
41+
case .never:
42+
"never"
43+
case .custom:
44+
"custom"
45+
}
46+
}
47+
48+
// MARK: Initialization
49+
50+
/// Initialize a `SessionTimeoutType` using a string of the raw value.
51+
///
52+
/// - Parameter rawValue: The string representation of the type raw value.
53+
///
54+
public init(rawValue: String?) {
55+
switch rawValue {
56+
case "custom":
57+
self = .custom
58+
case "immediately":
59+
self = .immediately
60+
case "never":
61+
self = .never
62+
case "onAppRestart",
63+
"onSystemLock":
64+
self = .onAppRestart
65+
default:
66+
self = .custom
67+
}
68+
}
69+
70+
/// Initialize a `SessionTimeoutType` using a SessionTimeoutValue that belongs to that type.
71+
///
72+
/// - Parameter value: The SessionTimeoutValue that belongs to the type.
73+
///
74+
public init(value: SessionTimeoutValue) {
75+
switch value {
76+
case .custom:
77+
self = .custom
78+
case .immediately:
79+
self = .immediately
80+
case .never:
81+
self = .never
82+
case .onAppRestart:
83+
self = .onAppRestart
84+
case .fifteenMinutes,
85+
.fiveMinutes,
86+
.fourHours,
87+
.oneHour,
88+
.oneMinute,
89+
.thirtyMinutes:
90+
self = .custom
91+
}
92+
}
93+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import BitwardenKit
2+
import XCTest
3+
4+
final class SessionTimeoutTypeTests: BitwardenTestCase {
5+
// MARK: Tests
6+
7+
/// `init(rawValue:)` returns the correct case for the given raw value string.
8+
func test_initFromRawValue() {
9+
XCTAssertEqual(SessionTimeoutType.immediately, SessionTimeoutType(rawValue: "immediately"))
10+
XCTAssertEqual(SessionTimeoutType.onAppRestart, SessionTimeoutType(rawValue: "onAppRestart"))
11+
// `onSystemLock` value maps to `onAppRestart` on mobile.
12+
XCTAssertEqual(SessionTimeoutType.onAppRestart, SessionTimeoutType(rawValue: "onSystemLock"))
13+
XCTAssertEqual(SessionTimeoutType.never, SessionTimeoutType(rawValue: "never"))
14+
XCTAssertEqual(SessionTimeoutType.custom, SessionTimeoutType(rawValue: "custom"))
15+
}
16+
17+
/// `init(rawValue:)` returns `.custom` for `nil` and unknown values (default case).
18+
func test_initFromRawValue_defaultCase() {
19+
// `nil` value maps to `custom` on mobile in support to legacy.
20+
XCTAssertEqual(SessionTimeoutType.custom, SessionTimeoutType(rawValue: nil))
21+
// Unknown/invalid strings map to `custom` (default case).
22+
XCTAssertEqual(SessionTimeoutType.custom, SessionTimeoutType(rawValue: "unknown"))
23+
XCTAssertEqual(SessionTimeoutType.custom, SessionTimeoutType(rawValue: "invalid"))
24+
XCTAssertEqual(SessionTimeoutType.custom, SessionTimeoutType(rawValue: ""))
25+
}
26+
27+
/// `init(value:)` returns the correct case for the given `SessionTimeoutValue`.
28+
func test_initFromSessionTimeoutValue() {
29+
XCTAssertEqual(SessionTimeoutType.immediately, SessionTimeoutType(value: .immediately))
30+
XCTAssertEqual(SessionTimeoutType.onAppRestart, SessionTimeoutType(value: .onAppRestart))
31+
XCTAssertEqual(SessionTimeoutType.never, SessionTimeoutType(value: .never))
32+
XCTAssertEqual(SessionTimeoutType.custom, SessionTimeoutType(value: .custom(123)))
33+
}
34+
35+
/// `init(value:)` returns `.custom` for all predefined timeout values.
36+
func test_initFromSessionTimeoutValue_predefined() {
37+
XCTAssertEqual(SessionTimeoutType.custom, SessionTimeoutType(value: .oneMinute))
38+
XCTAssertEqual(SessionTimeoutType.custom, SessionTimeoutType(value: .fiveMinutes))
39+
XCTAssertEqual(SessionTimeoutType.custom, SessionTimeoutType(value: .fifteenMinutes))
40+
XCTAssertEqual(SessionTimeoutType.custom, SessionTimeoutType(value: .thirtyMinutes))
41+
XCTAssertEqual(SessionTimeoutType.custom, SessionTimeoutType(value: .oneHour))
42+
XCTAssertEqual(SessionTimeoutType.custom, SessionTimeoutType(value: .fourHours))
43+
}
44+
45+
/// `rawValue` returns the correct string values.
46+
func test_rawValues() {
47+
XCTAssertEqual(SessionTimeoutType.immediately.rawValue, "immediately")
48+
XCTAssertEqual(SessionTimeoutType.onAppRestart.rawValue, "onAppRestart")
49+
XCTAssertEqual(SessionTimeoutType.never.rawValue, "never")
50+
XCTAssertEqual(SessionTimeoutType.custom.rawValue, "custom")
51+
}
52+
53+
/// `timeoutType` returns the correct string representation values.
54+
func test_timeoutType() {
55+
XCTAssertEqual(SessionTimeoutType.immediately.timeoutType, "immediately")
56+
XCTAssertEqual(SessionTimeoutType.onAppRestart.timeoutType, "on app restart")
57+
XCTAssertEqual(SessionTimeoutType.never.timeoutType, "never")
58+
XCTAssertEqual(SessionTimeoutType.custom.timeoutType, "custom")
59+
}
60+
}

BitwardenKit/UI/Platform/Application/Views/BitwardenMenuField.swift

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,10 +79,10 @@ public struct BitwardenMenuField<
7979
public var body: some View {
8080
VStack(alignment: .leading, spacing: 0) {
8181
menu
82+
.padding(.horizontal, 16)
8283

8384
footerView()
8485
}
85-
.padding(.horizontal, 16)
8686
.background(
8787
isEnabled
8888
? SharedAsset.Colors.backgroundSecondary.swiftUIColor
@@ -347,14 +347,16 @@ public struct BitwardenMenuField<
347347
@ViewBuilder
348348
private func footerView() -> some View {
349349
if let footerContent {
350+
Divider()
351+
.padding(.leading, 16)
350352
Group {
351-
Divider()
352353
if let footerContent = footerContent as? Text {
353354
footerContent.bitwardenMenuFooterText(topPadding: 12, bottomPadding: 12)
354355
} else {
355356
footerContent
356357
}
357358
}
359+
.padding(.horizontal, 16)
358360
}
359361
}
360362
}
@@ -425,6 +427,17 @@ private enum MenuPreviewOptions: CaseIterable, Menuable {
425427
.padding()
426428
}
427429
.background(Color(.systemGroupedBackground))
430+
431+
Group {
432+
BitwardenMenuField(
433+
title: "Animals",
434+
footer: "Your organization has set the maximum session timeout to 1 hour and 30 minutes.",
435+
options: MenuPreviewOptions.allCases,
436+
selection: .constant(.dog),
437+
)
438+
.padding()
439+
}
440+
.background(Color(.systemGroupedBackground))
428441
}
429442

430443
#Preview("Addititional Menu") {

BitwardenKit/UI/Platform/Application/Views/BitwardenToggle.swift

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,23 +38,24 @@ public struct BitwardenToggle<TitleContent: View, FooterContent: View>: View {
3838
.padding(.vertical, 12)
3939
.accessibilityIdentifier(accessibilityIdentifier ?? "")
4040
.accessibilityLabel(accessibilityLabel ?? "")
41+
.padding(.horizontal, 16)
4142

4243
if footer != nil || footerContent != nil {
4344
Divider()
44-
45+
.padding(.leading, 16)
4546
Group {
4647
if let footer {
4748
Text(footer)
4849
.styleGuide(.subheadline)
4950
.foregroundColor(Color(asset: SharedAsset.Colors.textSecondary))
51+
.padding(.vertical, 12)
5052
} else if let footerContent {
5153
footerContent
5254
}
5355
}
54-
.padding(.vertical, 12)
56+
.padding(.horizontal, 16)
5557
}
5658
}
57-
.padding(.horizontal, 16)
5859
}
5960

6061
// MARK: Initialization
@@ -174,6 +175,13 @@ public struct BitwardenToggle<TitleContent: View, FooterContent: View>: View {
174175
BitwardenToggle("Toggle", footer: "Footer text", isOn: .constant(false))
175176
.contentBlock()
176177

178+
BitwardenToggle(
179+
"Toggle",
180+
footer: "Footer text that's too long on purpose so truncation is triggered.",
181+
isOn: .constant(true),
182+
)
183+
.contentBlock()
184+
177185
BitwardenToggle("Toggle", isOn: .constant(false)) {
178186
Button("Custom footer content") {}
179187
.buttonStyle(.bitwardenBorderless)

0 commit comments

Comments
 (0)