Skip to content

Commit b896dbc

Browse files
[PM-19546] Add enable flight recorder screen (#1479)
1 parent 679ed57 commit b896dbc

18 files changed

+409
-5
lines changed
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/// An enum that represents how long to enable the flight recorder.
2+
///
3+
enum FlightRecorderLoggingDuration: CaseIterable, Menuable {
4+
/// The flight recorder is enabled for one hour.
5+
case oneHour
6+
7+
/// The flight recorder is enabled for eight hours.
8+
case eightHours
9+
10+
/// The flight recorder is enabled for 24 hours.
11+
case twentyFourHours
12+
13+
/// The flight recorder is enabled for one week.
14+
case oneWeek
15+
16+
var localizedName: String {
17+
switch self {
18+
case .oneHour: Localizations.oneHour
19+
case .eightHours: Localizations.xHours(8)
20+
case .twentyFourHours: Localizations.xHours(24)
21+
case .oneWeek: Localizations.oneWeek
22+
}
23+
}
24+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import XCTest
2+
3+
@testable import BitwardenShared
4+
5+
class FlightRecorderLoggingDurationTests: BitwardenTestCase {
6+
// MARK: Tests
7+
8+
/// `localizedName` returns the correct values.
9+
func test_localizedName() {
10+
XCTAssertEqual(FlightRecorderLoggingDuration.oneHour.localizedName, Localizations.oneHour)
11+
XCTAssertEqual(FlightRecorderLoggingDuration.eightHours.localizedName, Localizations.xHours(8))
12+
XCTAssertEqual(FlightRecorderLoggingDuration.twentyFourHours.localizedName, Localizations.xHours(24))
13+
XCTAssertEqual(FlightRecorderLoggingDuration.oneWeek.localizedName, Localizations.oneWeek)
14+
}
15+
}

BitwardenShared/UI/Platform/Application/Support/Localizations/en.lproj/Localizable.strings

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1153,3 +1153,11 @@
11531153
"VerifyYourIdentity"="Verify your identity";
11541154
"ShareErrorDetails" = "Share error details";
11551155
"FlightRecorder" = "Flight recorder";
1156+
"EnableFlightRecorder" = "Enable flight recorder";
1157+
"ExperiencingAnIssue" = "Experiencing an issue?";
1158+
"EnableTemporaryLoggingDescriptionLong" = "Enable temporary logging to collect and inspect logs locally. When enabled, application states and network calls may be logged—never sensitive vault information.";
1159+
"ToGetStartedSetALoggingDurationDescriptionLong" = "To get started, set a logging duration. Logging will automatically turn off after this period, giving you time to capture activity while reproducing any issues.";
1160+
"LoggingDuration" = "Logging duration";
1161+
"LogsWillBeAutomaticallyDeletedAfter30DaysDescriptionLong" = "Logs will be automatically deleted after 30 days. Bitwarden is only able to access your log data when you share it.";
1162+
"ForDetailsOnWhatIsAndIsntLoggedVisitTheBitwardenHelpCenter" = "For details on what is and isn’t logged, visit the **[Bitwarden help center](%1$@)**.";
1163+
"OneWeek" = "1 week";

BitwardenShared/UI/Platform/Settings/Settings/About/AboutProcessor.swift

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,12 @@ final class AboutProcessor: StateProcessor<AboutState, AboutAction, AboutEffect>
7878
case let .toastShown(newValue):
7979
state.toast = newValue
8080
case let .toggleFlightRecorder(isOn):
81-
state.isFlightRecorderToggleOn = isOn
81+
if isOn {
82+
coordinator.navigate(to: .enableFlightRecorder)
83+
} else {
84+
// TODO: PM-19577 Turn logging off
85+
state.isFlightRecorderToggleOn = isOn
86+
}
8287
case let .toggleSubmitCrashLogs(isOn):
8388
state.isSubmitCrashLogsToggleOn = isOn
8489
services.errorReporter.isEnabled = isOn

BitwardenShared/UI/Platform/Settings/Settings/About/AboutProcessorTests.swift

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -157,14 +157,25 @@ class AboutProcessorTests: BitwardenTestCase {
157157
XCTAssertNil(subject.state.toast)
158158
}
159159

160-
/// `receive(_:)` with action `.isFlightRecorderToggleOn` updates the toggle value in the state.
160+
/// `receive(_:)` with action `.isFlightRecorderToggleOn` disables the flight recorder when toggled off.
161161
@MainActor
162-
func test_receive_toggleFlightRecorder() {
162+
func test_receive_toggleFlightRecorder_off() {
163+
subject.state.isFlightRecorderToggleOn = true
164+
165+
subject.receive(.toggleFlightRecorder(false))
166+
167+
XCTAssertFalse(subject.state.isFlightRecorderToggleOn)
168+
}
169+
170+
/// `receive(_:)` with action `.isFlightRecorderToggleOn` navigates to the enable flight
171+
/// recorder screen when toggled on.
172+
@MainActor
173+
func test_receive_toggleFlightRecorder_on() {
163174
XCTAssertFalse(subject.state.isFlightRecorderToggleOn)
164175

165176
subject.receive(.toggleFlightRecorder(true))
166177

167-
XCTAssertTrue(subject.state.isFlightRecorderToggleOn)
178+
XCTAssertEqual(coordinator.routes, [.enableFlightRecorder])
168179
}
169180

170181
/// `receive(_:)` with action `.isSubmitCrashLogsToggleOn` updates the toggle value in the state.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// MARK: - EnableFlightRecorderAction
2+
3+
/// Actions handled by the `EnableFlightRecorderProcessor`.
4+
///
5+
enum EnableFlightRecorderAction: Equatable {
6+
/// Dismiss the sheet.
7+
case dismiss
8+
9+
/// The logging duration value has changed.
10+
case loggingDurationChanged(FlightRecorderLoggingDuration)
11+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
// MARK: - EnableFlightRecorderEffect
2+
3+
/// Effects that can be processed by the `EnableFlightRecorderProcessor`.
4+
///
5+
enum EnableFlightRecorderEffect: Equatable {
6+
/// The save button was tapped.
7+
case save
8+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import Foundation
2+
3+
// MARK: - EnableFlightRecorderProcessor
4+
5+
/// The processor used to manage state and handle actions for the `EnableFlightRecorderView`.
6+
///
7+
final class EnableFlightRecorderProcessor: StateProcessor<
8+
EnableFlightRecorderState,
9+
EnableFlightRecorderAction,
10+
EnableFlightRecorderEffect
11+
> {
12+
// MARK: Types
13+
14+
typealias Services = HasErrorReporter
15+
16+
// MARK: Private Properties
17+
18+
/// The `Coordinator` that handles navigation.
19+
private let coordinator: AnyCoordinator<SettingsRoute, SettingsEvent>
20+
21+
/// The services used by this processor.
22+
private var services: Services
23+
24+
// MARK: Initialization
25+
26+
/// Creates a new `EnableFlightRecorderProcessor`.
27+
///
28+
/// - Parameters:
29+
/// - coordinator: The `Coordinator` that handles navigation.
30+
/// - services: The services used by the processor.
31+
/// - state: The initial state of the processor.
32+
///
33+
init(
34+
coordinator: AnyCoordinator<SettingsRoute, SettingsEvent>,
35+
services: Services,
36+
state: EnableFlightRecorderState
37+
) {
38+
self.coordinator = coordinator
39+
self.services = services
40+
super.init(state: state)
41+
}
42+
43+
// MARK: Methods
44+
45+
override func perform(_ effect: EnableFlightRecorderEffect) async {
46+
switch effect {
47+
case .save:
48+
await saveAndEnableFlightRecorder()
49+
}
50+
}
51+
52+
override func receive(_ action: EnableFlightRecorderAction) {
53+
switch action {
54+
case .dismiss:
55+
coordinator.navigate(to: .dismiss)
56+
case let .loggingDurationChanged(loggingDuration):
57+
state.loggingDuration = loggingDuration
58+
}
59+
}
60+
61+
// MARK: Private Methods
62+
63+
/// Saves the logging duration and enables the flight recorder.
64+
///
65+
private func saveAndEnableFlightRecorder() async {
66+
// TODO: PM-19577 Enable logging
67+
coordinator.navigate(to: .dismiss)
68+
}
69+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import SnapshotTesting
2+
import XCTest
3+
4+
@testable import BitwardenShared
5+
6+
class EnableFlightRecorderProcessorTests: BitwardenTestCase {
7+
// MARK: Properties
8+
9+
var coordinator: MockCoordinator<SettingsRoute, SettingsEvent>!
10+
var subject: EnableFlightRecorderProcessor!
11+
12+
// MARK: Setup & Teardown
13+
14+
override func setUp() {
15+
super.setUp()
16+
17+
coordinator = MockCoordinator()
18+
19+
subject = EnableFlightRecorderProcessor(
20+
coordinator: coordinator.asAnyCoordinator(),
21+
services: ServiceContainer.withMocks(),
22+
state: EnableFlightRecorderState()
23+
)
24+
}
25+
26+
override func tearDown() {
27+
super.tearDown()
28+
29+
coordinator = nil
30+
subject = nil
31+
}
32+
33+
// MARK: Tests
34+
35+
/// `perform(_:)` with `.save` dismisses the view.
36+
@MainActor
37+
func test_perform_save() async {
38+
await subject.perform(.save)
39+
XCTAssertEqual(coordinator.routes.last, .dismiss)
40+
}
41+
42+
/// `receive(_:)` with `.dismiss` dismisses the view.
43+
@MainActor
44+
func test_receive_dismiss() {
45+
subject.receive(.dismiss)
46+
XCTAssertEqual(coordinator.routes.last, .dismiss)
47+
}
48+
49+
/// `receive(_:)` with `.loggingDurationChanged(:)` updates the state's logging duration value.
50+
@MainActor
51+
func test_receive_loggingDurationChanged() async throws {
52+
subject.receive(.loggingDurationChanged(.oneHour))
53+
XCTAssertEqual(subject.state.loggingDuration, .oneHour)
54+
55+
subject.receive(.loggingDurationChanged(.eightHours))
56+
XCTAssertEqual(subject.state.loggingDuration, .eightHours)
57+
}
58+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// MARK: - EnableFlightRecorderState
2+
3+
/// An object that defines the current state of the `EnableFlightRecorderView`.
4+
///
5+
struct EnableFlightRecorderState {
6+
// MARK: Properties
7+
8+
/// The selected logging duration value.
9+
var loggingDuration = FlightRecorderLoggingDuration.twentyFourHours
10+
}

0 commit comments

Comments
 (0)