From e8d7c67d7805b157162332fdc30b100502368f78 Mon Sep 17 00:00:00 2001 From: Matt Czech Date: Thu, 13 Nov 2025 12:40:59 -0600 Subject: [PATCH] [PM-26063] Move FlightRecorder into BitwardenKit (#2133) --- .../TestHelpers/ServiceContainer.swift | 6 ++ .../Extensions/FileManager+Extensions.swift | 12 +++ .../Platform/Extensions/Task+Extensions.swift | 17 +++ .../Core/Platform/Extensions/URL.swift | 36 +++++++ .../Models/Data/FlightRecorderData.swift | 47 ++++++--- .../Models/Data/FlightRecorderDataTests.swift | 2 +- .../FlightRecorderLogMetadata+Fixtures.swift | 5 +- .../Domain/FlightRecorderLogMetadata.swift | 5 +- .../FlightRecorderLogMetadataTests.swift | 4 +- .../Enum/FlightRecorderLoggingDuration.swift | 5 +- .../FlightRecorderLoggingDurationTests.swift | 4 +- .../API/FlightRecorderHTTPLogger.swift | 8 +- .../API/FlightRecorderHTTPLoggerTests.swift | 3 +- .../Platform/Services/FlightRecorder.swift | 57 ++++++---- .../Services/FlightRecorderStateService.swift | 17 +++ .../Services/FlightRecorderTests.swift | 35 ++++--- .../Services/Mocks/MockFlightRecorder.swift | 64 ++++++++++++ .../MockFlightRecorderStateService.swift | 21 ++++ .../Core/Platform/Services/Services.swift | 7 ++ .../Core/Platform/Utilities/Constants.swift | 4 + .../Core/Platform/Utilities/FileManager.swift | 10 +- .../Platform/Utilities/FileManagerTests.swift | 2 +- .../Utilities/Mocks/MockFileManager.swift | 63 ++++++++++++ .../FlightRecorder/Alert+FlightRecorder.swift | 27 +++++ .../AlertFlightRecorderTests.swift | 44 ++++++++ .../EnableFlightRecorderAction.swift | 0 .../EnableFlightRecorderEffect.swift | 0 .../EnableFlightRecorderProcessor.swift | 5 +- .../EnableFlightRecorderProcessorTests.swift | 4 +- .../EnableFlightRecorderState.swift | 0 ...ableFlightRecorderView+SnapshotTests.swift | 3 +- ...lightRecorderView+ViewInspectorTests.swift | 3 +- .../EnableFlightRecorderView.swift | 1 - .../test_snapshot_enableFlightRecorder.1.png | Bin .../test_snapshot_enableFlightRecorder.2.png | Bin .../test_snapshot_enableFlightRecorder.3.png | Bin .../FlightRecorderCoordinator.swift | 97 ++++++++++++++++++ .../FlightRecorderCoordinatorTests.swift | 86 ++++++++++++++++ .../FlightRecorderLogsAction.swift | 2 - .../FlightRecorderLogsEffect.swift | 0 .../FlightRecorderLogsProcessor.swift | 5 +- .../FlightRecorderLogsProcessorTests.swift | 5 +- .../FlightRecorderLogsState.swift | 2 - ...FlightRecorderLogsView+SnapshotTests.swift | 3 +- ...tRecorderLogsView+ViewInspectorTests.swift | 4 +- .../FlightRecorderLogsView.swift | 3 +- ...st_snapshot_flightRecorderLogs_empty.1.png | Bin ...st_snapshot_flightRecorderLogs_empty.2.png | Bin ...st_snapshot_flightRecorderLogs_empty.3.png | Bin ...napshot_flightRecorderLogs_populated.1.png | Bin ...napshot_flightRecorderLogs_populated.2.png | Bin ...napshot_flightRecorderLogs_populated.3.png | Bin .../FlightRecorder/FlightRecorderModule.swift | 16 +++ .../FlightRecorder/FlightRecorderRoute.swift | 20 ++++ .../Illustrations.xcassets/Contents.json | 6 ++ .../secure-devices.imageset/Contents.json | 25 +++++ .../secure-devices-dark.pdf | Bin 0 -> 3696 bytes .../secure-devices.pdf | Bin 0 -> 3691 bytes .../Extensions/FileManager+Extensions.swift | 9 -- .../Platform/Extensions/Task+Extensions.swift | 11 -- .../Core/Platform/Extensions/URL.swift | 36 ------- .../Platform/Services/ServiceContainer.swift | 2 +- .../Core/Platform/Services/Services.swift | 9 -- .../Core/Platform/Services/StateService.swift | 2 +- .../TestHelpers/MockFlightRecorder.swift | 64 ------------ .../Core/Platform/Utilities/Constants.swift | 4 - .../TestHelpers/MockFileManager.swift | 61 ----------- .../UI/Platform/Application/AppModule.swift | 14 +++ .../Settings/Extensions/Alert+Settings.swift | 20 ---- .../Extensions/AlertSettingsTests.swift | 36 ------- .../Settings/About/AboutProcessor.swift | 4 +- .../Settings/About/AboutProcessorTests.swift | 4 +- .../AboutStateTests.swift | 1 + .../Settings/SettingsCoordinator.swift | 37 ++----- .../Settings/SettingsCoordinatorTests.swift | 34 ++---- .../UI/Platform/Settings/SettingsRoute.swift | 10 +- GlobalTestHelpers/MockAppModule.swift | 8 ++ swiftgen-bwr.yml | 1 + 78 files changed, 744 insertions(+), 418 deletions(-) create mode 100644 BitwardenKit/Core/Platform/Extensions/FileManager+Extensions.swift create mode 100644 BitwardenKit/Core/Platform/Extensions/Task+Extensions.swift rename {BitwardenShared => BitwardenKit}/Core/Platform/Models/Data/FlightRecorderData.swift (70%) rename {BitwardenShared => BitwardenKit}/Core/Platform/Models/Data/FlightRecorderDataTests.swift (99%) rename {BitwardenShared => BitwardenKit}/Core/Platform/Models/Domain/Fixtures/FlightRecorderLogMetadata+Fixtures.swift (87%) rename {BitwardenShared => BitwardenKit}/Core/Platform/Models/Domain/FlightRecorderLogMetadata.swift (96%) rename {BitwardenShared => BitwardenKit}/Core/Platform/Models/Domain/FlightRecorderLogMetadataTests.swift (99%) rename {BitwardenShared => BitwardenKit}/Core/Platform/Models/Enum/FlightRecorderLoggingDuration.swift (93%) rename {BitwardenShared => BitwardenKit}/Core/Platform/Models/Enum/FlightRecorderLoggingDurationTests.swift (98%) rename {BitwardenShared => BitwardenKit}/Core/Platform/Services/API/FlightRecorderHTTPLogger.swift (78%) rename {BitwardenShared => BitwardenKit}/Core/Platform/Services/API/FlightRecorderHTTPLoggerTests.swift (97%) rename {BitwardenShared => BitwardenKit}/Core/Platform/Services/FlightRecorder.swift (90%) create mode 100644 BitwardenKit/Core/Platform/Services/FlightRecorderStateService.swift rename {BitwardenShared => BitwardenKit}/Core/Platform/Services/FlightRecorderTests.swift (95%) create mode 100644 BitwardenKit/Core/Platform/Services/Mocks/MockFlightRecorder.swift create mode 100644 BitwardenKit/Core/Platform/Services/Mocks/MockFlightRecorderStateService.swift rename {BitwardenShared => BitwardenKit}/Core/Platform/Utilities/FileManager.swift (87%) rename {BitwardenShared => BitwardenKit}/Core/Platform/Utilities/FileManagerTests.swift (98%) create mode 100644 BitwardenKit/Core/Platform/Utilities/Mocks/MockFileManager.swift create mode 100644 BitwardenKit/UI/Platform/Settings/FlightRecorder/Alert+FlightRecorder.swift create mode 100644 BitwardenKit/UI/Platform/Settings/FlightRecorder/AlertFlightRecorderTests.swift rename {BitwardenShared/UI/Platform/Settings/Settings/About => BitwardenKit/UI/Platform/Settings/FlightRecorder}/EnableFlightRecorder/EnableFlightRecorderAction.swift (100%) rename {BitwardenShared/UI/Platform/Settings/Settings/About => BitwardenKit/UI/Platform/Settings/FlightRecorder}/EnableFlightRecorder/EnableFlightRecorderEffect.swift (100%) rename {BitwardenShared/UI/Platform/Settings/Settings/About => BitwardenKit/UI/Platform/Settings/FlightRecorder}/EnableFlightRecorder/EnableFlightRecorderProcessor.swift (92%) rename {BitwardenShared/UI/Platform/Settings/Settings/About => BitwardenKit/UI/Platform/Settings/FlightRecorder}/EnableFlightRecorder/EnableFlightRecorderProcessorTests.swift (96%) rename {BitwardenShared/UI/Platform/Settings/Settings/About => BitwardenKit/UI/Platform/Settings/FlightRecorder}/EnableFlightRecorder/EnableFlightRecorderState.swift (100%) rename {BitwardenShared/UI/Platform/Settings/Settings/About => BitwardenKit/UI/Platform/Settings/FlightRecorder}/EnableFlightRecorder/EnableFlightRecorderView+SnapshotTests.swift (95%) rename {BitwardenShared/UI/Platform/Settings/Settings/About => BitwardenKit/UI/Platform/Settings/FlightRecorder}/EnableFlightRecorder/EnableFlightRecorderView+ViewInspectorTests.swift (97%) rename {BitwardenShared/UI/Platform/Settings/Settings/About => BitwardenKit/UI/Platform/Settings/FlightRecorder}/EnableFlightRecorder/EnableFlightRecorderView.swift (99%) rename {BitwardenShared/UI/Platform/Settings/Settings/About => BitwardenKit/UI/Platform/Settings/FlightRecorder}/EnableFlightRecorder/__Snapshots__/EnableFlightRecorderViewTests/test_snapshot_enableFlightRecorder.1.png (100%) rename {BitwardenShared/UI/Platform/Settings/Settings/About => BitwardenKit/UI/Platform/Settings/FlightRecorder}/EnableFlightRecorder/__Snapshots__/EnableFlightRecorderViewTests/test_snapshot_enableFlightRecorder.2.png (100%) rename {BitwardenShared/UI/Platform/Settings/Settings/About => BitwardenKit/UI/Platform/Settings/FlightRecorder}/EnableFlightRecorder/__Snapshots__/EnableFlightRecorderViewTests/test_snapshot_enableFlightRecorder.3.png (100%) create mode 100644 BitwardenKit/UI/Platform/Settings/FlightRecorder/FlightRecorderCoordinator.swift create mode 100644 BitwardenKit/UI/Platform/Settings/FlightRecorder/FlightRecorderCoordinatorTests.swift rename {BitwardenShared/UI/Platform/Settings/Settings/About => BitwardenKit/UI/Platform/Settings/FlightRecorder}/FlightRecorderLogs/FlightRecorderLogsAction.swift (96%) rename {BitwardenShared/UI/Platform/Settings/Settings/About => BitwardenKit/UI/Platform/Settings/FlightRecorder}/FlightRecorderLogs/FlightRecorderLogsEffect.swift (100%) rename {BitwardenShared/UI/Platform/Settings/Settings/About => BitwardenKit/UI/Platform/Settings/FlightRecorder}/FlightRecorderLogs/FlightRecorderLogsProcessor.swift (96%) rename {BitwardenShared/UI/Platform/Settings/Settings/About => BitwardenKit/UI/Platform/Settings/FlightRecorder}/FlightRecorderLogs/FlightRecorderLogsProcessorTests.swift (98%) rename {BitwardenShared/UI/Platform/Settings/Settings/About => BitwardenKit/UI/Platform/Settings/FlightRecorder}/FlightRecorderLogs/FlightRecorderLogsState.swift (96%) rename {BitwardenShared/UI/Platform/Settings/Settings/About => BitwardenKit/UI/Platform/Settings/FlightRecorder}/FlightRecorderLogs/FlightRecorderLogsView+SnapshotTests.swift (98%) rename {BitwardenShared/UI/Platform/Settings/Settings/About => BitwardenKit/UI/Platform/Settings/FlightRecorder}/FlightRecorderLogs/FlightRecorderLogsView+ViewInspectorTests.swift (98%) rename {BitwardenShared/UI/Platform/Settings/Settings/About => BitwardenKit/UI/Platform/Settings/FlightRecorder}/FlightRecorderLogs/FlightRecorderLogsView.swift (98%) rename {BitwardenShared/UI/Platform/Settings/Settings/About => BitwardenKit/UI/Platform/Settings/FlightRecorder}/FlightRecorderLogs/__Snapshots__/FlightRecorderLogsViewTests/test_snapshot_flightRecorderLogs_empty.1.png (100%) rename {BitwardenShared/UI/Platform/Settings/Settings/About => BitwardenKit/UI/Platform/Settings/FlightRecorder}/FlightRecorderLogs/__Snapshots__/FlightRecorderLogsViewTests/test_snapshot_flightRecorderLogs_empty.2.png (100%) rename {BitwardenShared/UI/Platform/Settings/Settings/About => BitwardenKit/UI/Platform/Settings/FlightRecorder}/FlightRecorderLogs/__Snapshots__/FlightRecorderLogsViewTests/test_snapshot_flightRecorderLogs_empty.3.png (100%) rename {BitwardenShared/UI/Platform/Settings/Settings/About => BitwardenKit/UI/Platform/Settings/FlightRecorder}/FlightRecorderLogs/__Snapshots__/FlightRecorderLogsViewTests/test_snapshot_flightRecorderLogs_populated.1.png (100%) rename {BitwardenShared/UI/Platform/Settings/Settings/About => BitwardenKit/UI/Platform/Settings/FlightRecorder}/FlightRecorderLogs/__Snapshots__/FlightRecorderLogsViewTests/test_snapshot_flightRecorderLogs_populated.2.png (100%) rename {BitwardenShared/UI/Platform/Settings/Settings/About => BitwardenKit/UI/Platform/Settings/FlightRecorder}/FlightRecorderLogs/__Snapshots__/FlightRecorderLogsViewTests/test_snapshot_flightRecorderLogs_populated.3.png (100%) create mode 100644 BitwardenKit/UI/Platform/Settings/FlightRecorder/FlightRecorderModule.swift create mode 100644 BitwardenKit/UI/Platform/Settings/FlightRecorder/FlightRecorderRoute.swift create mode 100644 BitwardenResources/Illustrations.xcassets/Contents.json create mode 100644 BitwardenResources/Illustrations.xcassets/secure-devices.imageset/Contents.json create mode 100644 BitwardenResources/Illustrations.xcassets/secure-devices.imageset/secure-devices-dark.pdf create mode 100644 BitwardenResources/Illustrations.xcassets/secure-devices.imageset/secure-devices.pdf delete mode 100644 BitwardenShared/Core/Platform/Extensions/Task+Extensions.swift delete mode 100644 BitwardenShared/Core/Platform/Services/TestHelpers/MockFlightRecorder.swift delete mode 100644 BitwardenShared/Core/Platform/Utilities/TestHelpers/MockFileManager.swift rename BitwardenShared/UI/Platform/Settings/Settings/About/{FlightRecorderLogs => }/AboutStateTests.swift (98%) diff --git a/BitwardenKit/Application/TestHelpers/ServiceContainer.swift b/BitwardenKit/Application/TestHelpers/ServiceContainer.swift index 9defca499..6d94a3563 100644 --- a/BitwardenKit/Application/TestHelpers/ServiceContainer.swift +++ b/BitwardenKit/Application/TestHelpers/ServiceContainer.swift @@ -7,6 +7,7 @@ typealias Services = HasConfigService & HasEnvironmentService & HasErrorReportBuilder & HasErrorReporter + & HasFlightRecorder & HasTimeProvider /// A service container used for testing processors within `BitwardenKitTests`. @@ -16,6 +17,7 @@ class ServiceContainer: Services { let environmentService: EnvironmentService let errorReportBuilder: any ErrorReportBuilder let errorReporter: ErrorReporter + let flightRecorder: FlightRecorder let timeProvider: TimeProvider required init( @@ -23,12 +25,14 @@ class ServiceContainer: Services { environmentService: EnvironmentService, errorReportBuilder: ErrorReportBuilder, errorReporter: ErrorReporter, + flightRecorder: FlightRecorder, timeProvider: TimeProvider, ) { self.configService = configService self.errorReportBuilder = errorReportBuilder self.environmentService = environmentService self.errorReporter = errorReporter + self.flightRecorder = flightRecorder self.timeProvider = timeProvider } } @@ -39,6 +43,7 @@ extension ServiceContainer { errorReportBuilder: ErrorReportBuilder = MockErrorReportBuilder(), environmentService: EnvironmentService = MockEnvironmentService(), errorReporter: ErrorReporter = MockErrorReporter(), + flightRecorder: FlightRecorder = MockFlightRecorder(), timeProvider: TimeProvider = MockTimeProvider(.currentTime), ) -> ServiceContainer { self.init( @@ -46,6 +51,7 @@ extension ServiceContainer { environmentService: environmentService, errorReportBuilder: errorReportBuilder, errorReporter: errorReporter, + flightRecorder: flightRecorder, timeProvider: timeProvider, ) } diff --git a/BitwardenKit/Core/Platform/Extensions/FileManager+Extensions.swift b/BitwardenKit/Core/Platform/Extensions/FileManager+Extensions.swift new file mode 100644 index 000000000..f491124a3 --- /dev/null +++ b/BitwardenKit/Core/Platform/Extensions/FileManager+Extensions.swift @@ -0,0 +1,12 @@ +import Foundation + +extension FileManager { + /// Returns a URL for the directory containing flight recorder logs. + /// + /// - Returns: A URL for a directory to store flight recorder logs, or `nil` if the container URL is unavailable. + /// + func flightRecorderLogURL() throws -> URL? { + containerURL(forSecurityApplicationGroupIdentifier: Bundle.main.groupIdentifier)? + .appendingPathComponent("FlightRecorderLogs", isDirectory: true) + } +} diff --git a/BitwardenKit/Core/Platform/Extensions/Task+Extensions.swift b/BitwardenKit/Core/Platform/Extensions/Task+Extensions.swift new file mode 100644 index 000000000..5c2149e94 --- /dev/null +++ b/BitwardenKit/Core/Platform/Extensions/Task+Extensions.swift @@ -0,0 +1,17 @@ +import Foundation + +public extension Task where Success == Never, Failure == Never { + /// Suspends the current task for at least the specified duration in seconds. + /// + /// - Parameters: + /// - delay: The number of seconds to sleep. + /// - tolerance: The acceptable tolerance for the sleep duration in seconds. Defaults to 1 second. + /// + static func sleep(forSeconds delay: Double, tolerance: Double = 1) async throws { + if #available(iOS 16.0, *) { + try await sleep(for: .seconds(delay), tolerance: .seconds(tolerance)) + } else { + try await sleep(nanoseconds: UInt64(delay * Double(NSEC_PER_SEC))) + } + } +} diff --git a/BitwardenKit/Core/Platform/Extensions/URL.swift b/BitwardenKit/Core/Platform/Extensions/URL.swift index ef9787200..297a0d660 100644 --- a/BitwardenKit/Core/Platform/Extensions/URL.swift +++ b/BitwardenKit/Core/Platform/Extensions/URL.swift @@ -31,4 +31,40 @@ public extension URL { guard absoluteString.hasPrefix(prefix) else { return absoluteString } return String(absoluteString.dropFirst(prefix.count)) } + + // MARK: Methods + + /// Creates a new `URL` appending the provided query items to the url. + /// + /// On iOS 16+, this method uses the method with the same name in Foundation. On iOS 15, this method + /// uses `URLComponents` to add the query items to the new url. + /// + /// - Parameter queryItems: A list of `URLQueryItem`s to add to this url. + /// - Returns: A new `URL` with the query items appended. + /// + func appending(queryItems: [URLQueryItem]) -> URL? { + if #available(iOS 16, *) { + // Set this variable to a non-optional `URL` type so that we are calling the function in Foundation, + // rather than recursively calling this method. + let url: URL = appending(queryItems: queryItems) + return url + } else { + guard var components = URLComponents(url: self, resolvingAgainstBaseURL: false) + else { return nil } + + components.queryItems = queryItems + return components.url + } + } + + /// Sets whether the file should be excluded from backups. + /// + /// - Parameter value: `true` if the file should be excluded from backups, or `false` otherwise. + /// + func setIsExcludedFromBackup(_ value: Bool) throws { + var url = self + var values = URLResourceValues() + values.isExcludedFromBackup = value + try url.setResourceValues(values) + } } diff --git a/BitwardenShared/Core/Platform/Models/Data/FlightRecorderData.swift b/BitwardenKit/Core/Platform/Models/Data/FlightRecorderData.swift similarity index 70% rename from BitwardenShared/Core/Platform/Models/Data/FlightRecorderData.swift rename to BitwardenKit/Core/Platform/Models/Data/FlightRecorderData.swift index 15dc65cad..fab0b279c 100644 --- a/BitwardenShared/Core/Platform/Models/Data/FlightRecorderData.swift +++ b/BitwardenKit/Core/Platform/Models/Data/FlightRecorderData.swift @@ -1,4 +1,3 @@ -import BitwardenKit import Foundation // MARK: - FlightRecorderData @@ -6,11 +5,11 @@ import Foundation /// A data model containing the persisted data necessary for the flight recorder. This stores the /// metadata for the active and any inactive logs. /// -struct FlightRecorderData: Codable, Equatable { +public struct FlightRecorderData: Codable, Equatable { // MARK: Properties /// The current log, if the flight recorder is active. - var activeLog: LogMetadata? { + public var activeLog: LogMetadata? { didSet { guard let oldValue, oldValue.id != activeLog?.id else { return } inactiveLogs.insert(oldValue, at: 0) @@ -19,48 +18,62 @@ struct FlightRecorderData: Codable, Equatable { /// A list of previously recorded and inactive logs, which remain available on device until they /// are deleted by the user or expire and are deleted by the app. - var inactiveLogs: [LogMetadata] = [] + public var inactiveLogs: [LogMetadata] = [] // MARK: Computed Properties /// The full list of logs containing the active and any inactive logs. - var allLogs: [LogMetadata] { + public var allLogs: [LogMetadata] { ([activeLog] + inactiveLogs).compactMap(\.self) } /// The upcoming date in which either the active log needs to end logging or an inactive log /// expires and needs to be removed. - var nextLogLifecycleDate: Date? { + public var nextLogLifecycleDate: Date? { let dates = [activeLog?.endDate].compactMap(\.self) + inactiveLogs.map(\.expirationDate) return dates.min() } + + // MARK: Initialization + + /// Initialize `FlightRecorderData`. + /// + /// - Parameters: + /// - activeLog: The current log, if the flight recorder is active. + /// - inactiveLogs: A list of previously recorded and inactive logs, which remain available + /// on device until they are deleted by the user or expire and are deleted by the app. + /// + public init(activeLog: LogMetadata? = nil, inactiveLogs: [LogMetadata] = []) { + self.activeLog = activeLog + self.inactiveLogs = inactiveLogs + } } -extension FlightRecorderData { +public extension FlightRecorderData { /// A data model containing the metadata for a flight recorder log. /// struct LogMetadata: Codable, Equatable, Identifiable { // MARK: Properties /// The duration for how long the flight recorder was enabled for the log. - let duration: FlightRecorderLoggingDuration + public let duration: FlightRecorderLoggingDuration /// The date when the logging will end. - let endDate: Date + public let endDate: Date /// The file name of the file on disk. - let fileName: String + public let fileName: String /// Whether the flight recorder toast banner has been dismissed for this log. - @DefaultFalse var isBannerDismissed = false + @DefaultFalse public var isBannerDismissed = false /// The date the logging was started. - let startDate: Date + public let startDate: Date // MARK: Computed Properties /// The date when the flight recorder log will expire and be deleted. - var expirationDate: Date { + public var expirationDate: Date { Calendar.current.date( byAdding: .day, value: Constants.flightRecorderLogExpirationDays, @@ -69,7 +82,7 @@ extension FlightRecorderData { } /// The formatted end date for the log. - var formattedEndDate: String { + public var formattedEndDate: String { let dateFormatter = DateFormatter() dateFormatter.dateStyle = .short dateFormatter.timeStyle = .none @@ -77,14 +90,14 @@ extension FlightRecorderData { } /// The formatted end time for the log. - var formattedEndTime: String { + public var formattedEndTime: String { let dateFormatter = DateFormatter() dateFormatter.dateStyle = .none dateFormatter.timeStyle = .short return dateFormatter.string(from: endDate) } - var id: String { + public var id: String { fileName } @@ -96,7 +109,7 @@ extension FlightRecorderData { /// - duration: The duration for how long the flight recorder was enabled for the log. /// - startDate: The date the logging was started. /// - init(duration: FlightRecorderLoggingDuration, startDate: Date) { + public init(duration: FlightRecorderLoggingDuration, startDate: Date) { self.duration = duration self.startDate = startDate diff --git a/BitwardenShared/Core/Platform/Models/Data/FlightRecorderDataTests.swift b/BitwardenKit/Core/Platform/Models/Data/FlightRecorderDataTests.swift similarity index 99% rename from BitwardenShared/Core/Platform/Models/Data/FlightRecorderDataTests.swift rename to BitwardenKit/Core/Platform/Models/Data/FlightRecorderDataTests.swift index ef3e708d5..3a09b5269 100644 --- a/BitwardenShared/Core/Platform/Models/Data/FlightRecorderDataTests.swift +++ b/BitwardenKit/Core/Platform/Models/Data/FlightRecorderDataTests.swift @@ -1,6 +1,6 @@ import XCTest -@testable import BitwardenShared +@testable import BitwardenKit class FlightRecorderDataTests: BitwardenTestCase { // MARK: Tests diff --git a/BitwardenShared/Core/Platform/Models/Domain/Fixtures/FlightRecorderLogMetadata+Fixtures.swift b/BitwardenKit/Core/Platform/Models/Domain/Fixtures/FlightRecorderLogMetadata+Fixtures.swift similarity index 87% rename from BitwardenShared/Core/Platform/Models/Domain/Fixtures/FlightRecorderLogMetadata+Fixtures.swift rename to BitwardenKit/Core/Platform/Models/Domain/Fixtures/FlightRecorderLogMetadata+Fixtures.swift index 9c782bc79..4efbf5ca9 100644 --- a/BitwardenShared/Core/Platform/Models/Domain/Fixtures/FlightRecorderLogMetadata+Fixtures.swift +++ b/BitwardenKit/Core/Platform/Models/Domain/Fixtures/FlightRecorderLogMetadata+Fixtures.swift @@ -1,8 +1,9 @@ import Foundation -@testable import BitwardenShared +@testable import BitwardenKit -extension FlightRecorderLogMetadata { +public extension FlightRecorderLogMetadata { + // swiftlint:disable:next missing_docs static func fixture( duration: FlightRecorderLoggingDuration = .twentyFourHours, endDate: Date = Date(year: 2025, month: 4, day: 4), diff --git a/BitwardenShared/Core/Platform/Models/Domain/FlightRecorderLogMetadata.swift b/BitwardenKit/Core/Platform/Models/Domain/FlightRecorderLogMetadata.swift similarity index 96% rename from BitwardenShared/Core/Platform/Models/Domain/FlightRecorderLogMetadata.swift rename to BitwardenKit/Core/Platform/Models/Domain/FlightRecorderLogMetadata.swift index 5381df343..a49ffa856 100644 --- a/BitwardenShared/Core/Platform/Models/Domain/FlightRecorderLogMetadata.swift +++ b/BitwardenKit/Core/Platform/Models/Domain/FlightRecorderLogMetadata.swift @@ -1,4 +1,3 @@ -import BitwardenKit import BitwardenResources import Foundation @@ -6,7 +5,7 @@ import Foundation /// A data model containing the metadata associated with a flight recorder log. /// -struct FlightRecorderLogMetadata: Equatable, Identifiable { +public struct FlightRecorderLogMetadata: Equatable, Identifiable { // MARK: Properties /// The duration for how long the flight recorder was enabled for the log. @@ -22,7 +21,7 @@ struct FlightRecorderLogMetadata: Equatable, Identifiable { let fileSize: String /// A unique identifier for the log. - let id: String + public let id: String /// Whether this represents the active log. let isActiveLog: Bool diff --git a/BitwardenShared/Core/Platform/Models/Domain/FlightRecorderLogMetadataTests.swift b/BitwardenKit/Core/Platform/Models/Domain/FlightRecorderLogMetadataTests.swift similarity index 99% rename from BitwardenShared/Core/Platform/Models/Domain/FlightRecorderLogMetadataTests.swift rename to BitwardenKit/Core/Platform/Models/Domain/FlightRecorderLogMetadataTests.swift index a9f28b53c..b0542d195 100644 --- a/BitwardenShared/Core/Platform/Models/Domain/FlightRecorderLogMetadataTests.swift +++ b/BitwardenKit/Core/Platform/Models/Domain/FlightRecorderLogMetadataTests.swift @@ -1,7 +1,7 @@ +import BitwardenResources import XCTest -import BitwardenResources -@testable import BitwardenShared +@testable import BitwardenKit class FlightRecorderLogMetadataTests: BitwardenTestCase { // MARK: Properties diff --git a/BitwardenShared/Core/Platform/Models/Enum/FlightRecorderLoggingDuration.swift b/BitwardenKit/Core/Platform/Models/Enum/FlightRecorderLoggingDuration.swift similarity index 93% rename from BitwardenShared/Core/Platform/Models/Enum/FlightRecorderLoggingDuration.swift rename to BitwardenKit/Core/Platform/Models/Enum/FlightRecorderLoggingDuration.swift index df0aadc87..c9f31392f 100644 --- a/BitwardenShared/Core/Platform/Models/Enum/FlightRecorderLoggingDuration.swift +++ b/BitwardenKit/Core/Platform/Models/Enum/FlightRecorderLoggingDuration.swift @@ -1,10 +1,9 @@ -import BitwardenKit import BitwardenResources import Foundation /// An enum that represents how long to enable the flight recorder. /// -enum FlightRecorderLoggingDuration: CaseIterable, Codable, Menuable { +public enum FlightRecorderLoggingDuration: CaseIterable, Codable, Menuable { /// The flight recorder is enabled for one hour. case oneHour @@ -17,7 +16,7 @@ enum FlightRecorderLoggingDuration: CaseIterable, Codable, Menuable { /// The flight recorder is enabled for one week. case oneWeek - var localizedName: String { + public var localizedName: String { switch self { case .oneHour: Localizations.xHours(1) case .eightHours: Localizations.xHours(8) diff --git a/BitwardenShared/Core/Platform/Models/Enum/FlightRecorderLoggingDurationTests.swift b/BitwardenKit/Core/Platform/Models/Enum/FlightRecorderLoggingDurationTests.swift similarity index 98% rename from BitwardenShared/Core/Platform/Models/Enum/FlightRecorderLoggingDurationTests.swift rename to BitwardenKit/Core/Platform/Models/Enum/FlightRecorderLoggingDurationTests.swift index 9ec4e67b6..aa0d32f72 100644 --- a/BitwardenShared/Core/Platform/Models/Enum/FlightRecorderLoggingDurationTests.swift +++ b/BitwardenKit/Core/Platform/Models/Enum/FlightRecorderLoggingDurationTests.swift @@ -1,7 +1,7 @@ +import BitwardenResources import XCTest -import BitwardenResources -@testable import BitwardenShared +@testable import BitwardenKit class FlightRecorderLoggingDurationTests: BitwardenTestCase { // MARK: Tests diff --git a/BitwardenShared/Core/Platform/Services/API/FlightRecorderHTTPLogger.swift b/BitwardenKit/Core/Platform/Services/API/FlightRecorderHTTPLogger.swift similarity index 78% rename from BitwardenShared/Core/Platform/Services/API/FlightRecorderHTTPLogger.swift rename to BitwardenKit/Core/Platform/Services/API/FlightRecorderHTTPLogger.swift index c71ffa045..6ed39f18b 100644 --- a/BitwardenShared/Core/Platform/Services/API/FlightRecorderHTTPLogger.swift +++ b/BitwardenKit/Core/Platform/Services/API/FlightRecorderHTTPLogger.swift @@ -4,7 +4,7 @@ import Networking /// An `HTTPLogger` that logs HTTP requests and responses to the flight recorder. /// -final class FlightRecorderHTTPLogger: HTTPLogger { +public final class FlightRecorderHTTPLogger: HTTPLogger { // MARK: Properties /// The service used by the application for recording temporary debug logs. @@ -16,19 +16,19 @@ final class FlightRecorderHTTPLogger: HTTPLogger { /// /// - Parameter flightRecorder: The service used by the application for recording temporary debug logs. /// - init(flightRecorder: FlightRecorder) { + public init(flightRecorder: FlightRecorder) { self.flightRecorder = flightRecorder } // MARK: HTTPLogger - func logRequest(_ httpRequest: HTTPRequest) async { + public func logRequest(_ httpRequest: HTTPRequest) async { await flightRecorder.log( "Request \(httpRequest.requestID): \(httpRequest.method.rawValue) \(httpRequest.url)", ) } - func logResponse(_ httpResponse: HTTPResponse) async { + public func logResponse(_ httpResponse: HTTPResponse) async { await flightRecorder.log( "Response \(httpResponse.requestID): \(httpResponse.url) \(httpResponse.statusCode)", ) diff --git a/BitwardenShared/Core/Platform/Services/API/FlightRecorderHTTPLoggerTests.swift b/BitwardenKit/Core/Platform/Services/API/FlightRecorderHTTPLoggerTests.swift similarity index 97% rename from BitwardenShared/Core/Platform/Services/API/FlightRecorderHTTPLoggerTests.swift rename to BitwardenKit/Core/Platform/Services/API/FlightRecorderHTTPLoggerTests.swift index aa230107b..d8215e5ac 100644 --- a/BitwardenShared/Core/Platform/Services/API/FlightRecorderHTTPLoggerTests.swift +++ b/BitwardenKit/Core/Platform/Services/API/FlightRecorderHTTPLoggerTests.swift @@ -1,8 +1,9 @@ +import BitwardenKitMocks import Networking import TestHelpers import XCTest -@testable import BitwardenShared +@testable import BitwardenKit class FlightRecorderHTTPLoggerTests: BitwardenTestCase { // MARK: Properties diff --git a/BitwardenShared/Core/Platform/Services/FlightRecorder.swift b/BitwardenKit/Core/Platform/Services/FlightRecorder.swift similarity index 90% rename from BitwardenShared/Core/Platform/Services/FlightRecorder.swift rename to BitwardenKit/Core/Platform/Services/FlightRecorder.swift index cb005473b..4c981e88f 100644 --- a/BitwardenShared/Core/Platform/Services/FlightRecorder.swift +++ b/BitwardenKit/Core/Platform/Services/FlightRecorder.swift @@ -1,4 +1,3 @@ -import BitwardenKit @preconcurrency import Combine import Foundation import OSLog @@ -10,7 +9,7 @@ import OSLog /// A protocol for a service which can temporarily be enabled to collect logs for debugging to a /// local file. /// -protocol FlightRecorder: Sendable, BitwardenLogger { +public protocol FlightRecorder: Sendable, BitwardenLogger { /// A publisher which publishes the active log of the flight recorder. /// /// - Returns: A publisher for the active log of the flight recorder. @@ -64,11 +63,25 @@ protocol FlightRecorder: Sendable, BitwardenLogger { func setFlightRecorderBannerDismissed() async } -extension FlightRecorder { +public extension FlightRecorder { + /// Appends a message to the active log, if logging is currently enabled. + /// + /// - Parameters: + /// - message: The message to append to the active log. + /// - file: The file that called the log method. + /// - line: The line number in the file that called the log method. + /// func log(_ message: String, file: String = #file, line: UInt = #line) async { await log(message, file: file, line: line) } + /// Appends a message to the active log, if logging is currently enabled. + /// + /// - Parameters: + /// - message: The message to append to the active log. + /// - file: The file that called the log method. + /// - line: The line number in the file that called the log method. + /// nonisolated func log(_ message: String, file: String, line: UInt) { Task { await log(message, file: file, line: line) @@ -81,6 +94,9 @@ extension FlightRecorder { /// An enumeration of errors thrown by a `FlightRecorder`. /// enum FlightRecorderError: Error { + /// The container URL for the app group is unavailable. + case containerURLUnavailable + /// The stored flight recorder data doesn't exist. case dataUnavailable @@ -119,6 +135,7 @@ extension FlightRecorderError: CustomNSError { case .logNotFound: 5 case .removeExpiredLogError: 6 case .writeMessageError: 7 + case .containerURLUnavailable: 8 } } @@ -146,7 +163,8 @@ extension FlightRecorderError: CustomNSError { extension FlightRecorderError: Equatable { static func == (lhs: FlightRecorderError, rhs: FlightRecorderError) -> Bool { switch (lhs, rhs) { - case (.dataUnavailable, .dataUnavailable), + case (.containerURLUnavailable, .containerURLUnavailable), + (.dataUnavailable, .dataUnavailable), (.deletionNotPermitted, .deletionNotPermitted), (.logNotFound, .logNotFound): true @@ -165,7 +183,7 @@ extension FlightRecorderError: Equatable { /// A default implementation of a `FlightRecorder`. /// -actor DefaultFlightRecorder { +public actor DefaultFlightRecorder { // MARK: Private Properties /// A subject containing the flight recorder data. This serves as a cache of the data after it @@ -190,7 +208,7 @@ actor DefaultFlightRecorder { private let fileManager: FileManagerProtocol /// The service used by the application to manage account state. - private let stateService: StateService + private let stateService: FlightRecorderStateService /// The service used to get the present time. private let timeProvider: TimeProvider @@ -213,12 +231,12 @@ actor DefaultFlightRecorder { /// - stateService: The service used by the application to manage account state. /// - timeProvider: The service used to get the present time. /// - init( + public init( appInfoService: AppInfoService, disableLogLifecycleTimerForTesting: Bool = false, errorReporter: ErrorReporter, fileManager: FileManagerProtocol = FileManager.default, - stateService: StateService, + stateService: FlightRecorderStateService, timeProvider: TimeProvider, ) { self.appInfoService = appInfoService @@ -371,7 +389,10 @@ actor DefaultFlightRecorder { /// - Returns: A URL for the log file. /// private func fileURL(for log: FlightRecorderData.LogMetadata) throws -> URL { - try FileManager.default.flightRecorderLogURL().appendingPathComponent(log.fileName) + guard let baseURL = try FileManager.default.flightRecorderLogURL() else { + throw FlightRecorderError.containerURLUnavailable + } + return baseURL.appendingPathComponent(log.fileName) } /// Gets the `FlightRecorderData`. If the data has already been loaded, it will be returned @@ -419,12 +440,12 @@ actor DefaultFlightRecorder { // MARK: - DefaultFlightRecorder + FlightRecorder extension DefaultFlightRecorder: FlightRecorder { - func activeLogPublisher() async -> AnyPublisher { + public func activeLogPublisher() async -> AnyPublisher { _ = await getFlightRecorderData() // Ensure data has already been loaded to the subject. return dataSubject.map { $0?.activeLog }.eraseToAnyPublisher() } - func deleteInactiveLogs() async throws { + public func deleteInactiveLogs() async throws { guard var data = await getFlightRecorderData() else { throw FlightRecorderError.dataUnavailable } @@ -437,7 +458,7 @@ extension DefaultFlightRecorder: FlightRecorder { await setFlightRecorderData(data) } - func deleteLog(_ log: FlightRecorderLogMetadata) async throws { + public func deleteLog(_ log: FlightRecorderLogMetadata) async throws { guard var data = await getFlightRecorderData() else { throw FlightRecorderError.dataUnavailable } @@ -453,13 +474,13 @@ extension DefaultFlightRecorder: FlightRecorder { await setFlightRecorderData(data) } - func disableFlightRecorder() async { + public func disableFlightRecorder() async { guard var data = await getFlightRecorderData() else { return } data.activeLog = nil await setFlightRecorderData(data) } - func enableFlightRecorder(duration: FlightRecorderLoggingDuration) async throws { + public func enableFlightRecorder(duration: FlightRecorderLoggingDuration) async throws { let log = FlightRecorderData.LogMetadata(duration: duration, startDate: timeProvider.presentTime) try await createLogFile(for: log) @@ -468,7 +489,7 @@ extension DefaultFlightRecorder: FlightRecorder { await setFlightRecorderData(data) } - func fetchLogs() async throws -> [FlightRecorderLogMetadata] { + public func fetchLogs() async throws -> [FlightRecorderLogMetadata] { guard let data = await getFlightRecorderData() else { return [] } return try data.allLogs.map { log in try FlightRecorderLogMetadata( @@ -484,12 +505,12 @@ extension DefaultFlightRecorder: FlightRecorder { } } - func isEnabledPublisher() async -> AnyPublisher { + public func isEnabledPublisher() async -> AnyPublisher { _ = await getFlightRecorderData() // Ensure data has already been loaded to the subject. return dataSubject.map { $0?.activeLog != nil }.eraseToAnyPublisher() } - func log(_ message: String, file: String, line: UInt) async { + public func log(_ message: String, file: String, line: UInt) async { guard var data = await getFlightRecorderData(), let log = data.activeLog else { return } Logger.flightRecorder.debug("\(message)") do { @@ -506,7 +527,7 @@ extension DefaultFlightRecorder: FlightRecorder { } } - func setFlightRecorderBannerDismissed() async { + public func setFlightRecorderBannerDismissed() async { guard var data = await getFlightRecorderData(), data.activeLog != nil else { return } data.activeLog?.isBannerDismissed = true await setFlightRecorderData(data) diff --git a/BitwardenKit/Core/Platform/Services/FlightRecorderStateService.swift b/BitwardenKit/Core/Platform/Services/FlightRecorderStateService.swift new file mode 100644 index 000000000..85367593e --- /dev/null +++ b/BitwardenKit/Core/Platform/Services/FlightRecorderStateService.swift @@ -0,0 +1,17 @@ +// MARK: - FlightRecorderStateService + +/// A protocol for a service that provides state management functionality required by the flight recorder. +/// +public protocol FlightRecorderStateService: ActiveAccountStateProvider { + /// Retrieves the persisted flight recorder data. + /// + /// - Returns: The stored `FlightRecorderData` if available, otherwise `nil`. + /// + func getFlightRecorderData() async -> FlightRecorderData? + + /// Persists the flight recorder data to storage. + /// + /// - Parameter data: The `FlightRecorderData` to persist, or `nil` to clear the stored data. + /// + func setFlightRecorderData(_ data: FlightRecorderData?) async +} diff --git a/BitwardenShared/Core/Platform/Services/FlightRecorderTests.swift b/BitwardenKit/Core/Platform/Services/FlightRecorderTests.swift similarity index 95% rename from BitwardenShared/Core/Platform/Services/FlightRecorderTests.swift rename to BitwardenKit/Core/Platform/Services/FlightRecorderTests.swift index 9375e1723..db6e0da47 100644 --- a/BitwardenShared/Core/Platform/Services/FlightRecorderTests.swift +++ b/BitwardenKit/Core/Platform/Services/FlightRecorderTests.swift @@ -4,7 +4,7 @@ import InlineSnapshotTesting import TestHelpers import XCTest -@testable import BitwardenShared +@testable import BitwardenKit // swiftlint:disable file_length @@ -15,7 +15,7 @@ class FlightRecorderTests: BitwardenTestCase { // swiftlint:disable:this type_bo var errorReporter: MockErrorReporter! var fileManager: MockFileManager! var logURL: URL! - var stateService: MockStateService! + var stateService: MockFlightRecorderStateService! var subject: FlightRecorder! var timeProvider: MockTimeProvider! @@ -42,11 +42,10 @@ class FlightRecorderTests: BitwardenTestCase { // swiftlint:disable:this type_bo appInfoService = MockAppInfoService() errorReporter = MockErrorReporter() fileManager = MockFileManager() - stateService = MockStateService() + stateService = MockFlightRecorderStateService() timeProvider = MockTimeProvider(.mockTime(Date(year: 2025, month: 1, day: 1))) - logURL = try XCTUnwrap(FileManager.default.flightRecorderLogURL() - .appendingPathComponent("flight_recorder_2025-01-01-00-00-00.txt")) + logURL = try flightRecorderLogURL().appendingPathComponent("flight_recorder_2025-01-01-00-00-00.txt") subject = makeSubject() } @@ -124,8 +123,8 @@ class FlightRecorderTests: BitwardenTestCase { // swiftlint:disable:this type_bo try XCTAssertEqual( fileManager.removeItemURLs, [ - FileManager.default.flightRecorderLogURL().appendingPathComponent(inactiveLog1.fileName), - FileManager.default.flightRecorderLogURL().appendingPathComponent(inactiveLog2.fileName), + flightRecorderLogURL().appendingPathComponent(inactiveLog1.fileName), + flightRecorderLogURL().appendingPathComponent(inactiveLog2.fileName), ], ) XCTAssertEqual(stateService.flightRecorderData, FlightRecorderData(activeLog: activeLog)) @@ -326,11 +325,10 @@ class FlightRecorderTests: BitwardenTestCase { // swiftlint:disable:this type_bo activeLog: activeLog, inactiveLogs: [inactiveLog1, inactiveLog2], ) - let flightRecorderLogURL = try FileManager.default.flightRecorderLogURL() let logs = try await subject.fetchLogs() - XCTAssertEqual( + try XCTAssertEqual( logs, [ FlightRecorderLogMetadata.fixture( @@ -341,7 +339,7 @@ class FlightRecorderTests: BitwardenTestCase { // swiftlint:disable:this type_bo id: activeLog.id, isActiveLog: true, startDate: Date(year: 2025, month: 1, day: 1), - url: flightRecorderLogURL.appendingPathComponent(activeLog.fileName), + url: flightRecorderLogURL().appendingPathComponent(activeLog.fileName), ), FlightRecorderLogMetadata.fixture( duration: .oneHour, @@ -351,7 +349,7 @@ class FlightRecorderTests: BitwardenTestCase { // swiftlint:disable:this type_bo id: inactiveLog1.id, isActiveLog: false, startDate: Date(year: 2025, month: 1, day: 2), - url: flightRecorderLogURL.appendingPathComponent(inactiveLog1.fileName), + url: flightRecorderLogURL().appendingPathComponent(inactiveLog1.fileName), ), FlightRecorderLogMetadata.fixture( duration: .oneWeek, @@ -361,7 +359,7 @@ class FlightRecorderTests: BitwardenTestCase { // swiftlint:disable:this type_bo id: inactiveLog2.id, isActiveLog: false, startDate: Date(year: 2025, month: 1, day: 3), - url: flightRecorderLogURL.appendingPathComponent(inactiveLog2.fileName), + url: flightRecorderLogURL().appendingPathComponent(inactiveLog2.fileName), ), ], ) @@ -483,7 +481,7 @@ class FlightRecorderTests: BitwardenTestCase { // swiftlint:disable:this type_bo // Expired active log is removed. try XCTAssertEqual( fileManager.removeItemURLs, - [FileManager.default.flightRecorderLogURL().appendingPathComponent(activeLog.fileName)], + [flightRecorderLogURL().appendingPathComponent(activeLog.fileName)], ) XCTAssertEqual( stateService.flightRecorderData, @@ -515,7 +513,7 @@ class FlightRecorderTests: BitwardenTestCase { // swiftlint:disable:this type_bo // Expired inactive log is removed. try XCTAssertEqual( fileManager.removeItemURLs, - [FileManager.default.flightRecorderLogURL().appendingPathComponent(expiredLog.fileName)], + [flightRecorderLogURL().appendingPathComponent(expiredLog.fileName)], ) XCTAssertEqual( stateService.flightRecorderData, @@ -538,7 +536,7 @@ class FlightRecorderTests: BitwardenTestCase { // swiftlint:disable:this type_bo XCTAssertEqual(error, .removeExpiredLogError(BitwardenTestError.example)) try XCTAssertEqual( fileManager.removeItemURLs, - [FileManager.default.flightRecorderLogURL().appendingPathComponent(inactiveLog1.fileName)], + [flightRecorderLogURL().appendingPathComponent(inactiveLog1.fileName)], ) XCTAssertEqual(stateService.flightRecorderData, FlightRecorderData()) } @@ -664,4 +662,11 @@ class FlightRecorderTests: BitwardenTestCase { // swiftlint:disable:this type_bo XCTAssertEqual(appendedMessage, "2025-01-01T00:00:00Z: Hello world!\n") XCTAssertEqual(stateService.flightRecorderData, FlightRecorderData(activeLog: activeLog)) } + + // MARK: Private + + /// Returns an unwrapped URL to the directory containing flight recorder logs. + private func flightRecorderLogURL() throws -> URL { + try XCTUnwrap(FileManager.default.flightRecorderLogURL()) + } } diff --git a/BitwardenKit/Core/Platform/Services/Mocks/MockFlightRecorder.swift b/BitwardenKit/Core/Platform/Services/Mocks/MockFlightRecorder.swift new file mode 100644 index 000000000..0201e0055 --- /dev/null +++ b/BitwardenKit/Core/Platform/Services/Mocks/MockFlightRecorder.swift @@ -0,0 +1,64 @@ +import Combine + +@testable import BitwardenKit + +@MainActor +public final class MockFlightRecorder: FlightRecorder { + public var activeLogSubject = CurrentValueSubject(nil) + public var deleteInactiveLogsCalled = false + public var deleteInactiveLogsResult: Result = .success(()) + public var deleteLogResult: Result = .success(()) + public var deleteLogLogs = [FlightRecorderLogMetadata]() + public var disableFlightRecorderCalled = false + public var enableFlightRecorderCalled = false + public var enableFlightRecorderDuration: FlightRecorderLoggingDuration? + public var enableFlightRecorderResult: Result = .success(()) + public var fetchLogsCalled = false + public var fetchLogsResult: Result<[FlightRecorderLogMetadata], Error> = .success([]) + public var isEnabledSubject = CurrentValueSubject(false) + public var logMessages = [String]() + public var setFlightRecorderBannerDismissedCalled = false + + public nonisolated init() {} + + public func activeLogPublisher() async -> AnyPublisher { + activeLogSubject.eraseToAnyPublisher() + } + + public func deleteInactiveLogs() async throws { + deleteInactiveLogsCalled = true + try deleteInactiveLogsResult.get() + } + + public func deleteLog(_ log: FlightRecorderLogMetadata) async throws { + deleteLogLogs.append(log) + try deleteLogResult.get() + } + + public func disableFlightRecorder() { + disableFlightRecorderCalled = true + } + + public func enableFlightRecorder(duration: FlightRecorderLoggingDuration) async throws { + enableFlightRecorderCalled = true + enableFlightRecorderDuration = duration + try enableFlightRecorderResult.get() + } + + public func fetchLogs() async throws -> [FlightRecorderLogMetadata] { + fetchLogsCalled = true + return try fetchLogsResult.get() + } + + public func isEnabledPublisher() async -> AnyPublisher { + isEnabledSubject.eraseToAnyPublisher() + } + + public func log(_ message: String, file: String, line: UInt) async { + logMessages.append(message) + } + + public func setFlightRecorderBannerDismissed() async { + setFlightRecorderBannerDismissedCalled = true + } +} diff --git a/BitwardenKit/Core/Platform/Services/Mocks/MockFlightRecorderStateService.swift b/BitwardenKit/Core/Platform/Services/Mocks/MockFlightRecorderStateService.swift new file mode 100644 index 000000000..ad001a9a5 --- /dev/null +++ b/BitwardenKit/Core/Platform/Services/Mocks/MockFlightRecorderStateService.swift @@ -0,0 +1,21 @@ +import BitwardenKit +import TestHelpers + +public class MockFlightRecorderStateService: FlightRecorderStateService { + public var activeAccountIdResult = Result.failure(BitwardenTestError.example) + public var flightRecorderData: FlightRecorderData? + + public init() {} + + public func getActiveAccountId() async throws -> String { + try activeAccountIdResult.get() + } + + public func getFlightRecorderData() async -> FlightRecorderData? { + flightRecorderData + } + + public func setFlightRecorderData(_ data: FlightRecorderData?) async { + flightRecorderData = data + } +} diff --git a/BitwardenKit/Core/Platform/Services/Services.swift b/BitwardenKit/Core/Platform/Services/Services.swift index a74d8f1ed..089704b14 100644 --- a/BitwardenKit/Core/Platform/Services/Services.swift +++ b/BitwardenKit/Core/Platform/Services/Services.swift @@ -28,6 +28,13 @@ public protocol HasErrorReporter { var errorReporter: ErrorReporter { get } } +/// Protocol for an object that provides a `FlightRecorder`. +/// +public protocol HasFlightRecorder { + /// The service used by the application for recording temporary debug logs. + var flightRecorder: FlightRecorder { get } +} + /// Protocol for an object that provides a `TimeProvider`. /// public protocol HasTimeProvider { diff --git a/BitwardenKit/Core/Platform/Utilities/Constants.swift b/BitwardenKit/Core/Platform/Utilities/Constants.swift index 22033d764..ea8f96cc4 100644 --- a/BitwardenKit/Core/Platform/Utilities/Constants.swift +++ b/BitwardenKit/Core/Platform/Utilities/Constants.swift @@ -17,6 +17,10 @@ public enum Constants { /// The device type, iOS = 1. public static let deviceType: DeviceType = 1 + /// The number of days that a flight recorder log will remain on the device after the end date + /// before being automatically deleted. + static let flightRecorderLogExpirationDays = 30 + /// The minimum number of minutes before attempting a server config sync again. public static let minimumConfigSyncInterval: TimeInterval = 60 * 60 // 60 minutes diff --git a/BitwardenShared/Core/Platform/Utilities/FileManager.swift b/BitwardenKit/Core/Platform/Utilities/FileManager.swift similarity index 87% rename from BitwardenShared/Core/Platform/Utilities/FileManager.swift rename to BitwardenKit/Core/Platform/Utilities/FileManager.swift index 27ccd992e..76ac84068 100644 --- a/BitwardenShared/Core/Platform/Utilities/FileManager.swift +++ b/BitwardenKit/Core/Platform/Utilities/FileManager.swift @@ -4,7 +4,7 @@ import Foundation /// A protocol for an object that is used to perform filesystem tasks. /// -protocol FileManagerProtocol: AnyObject { +public protocol FileManagerProtocol: AnyObject { /// Appends the given data to the file at the specified URL. /// /// - Parameters: @@ -55,22 +55,22 @@ protocol FileManagerProtocol: AnyObject { // MARK: - FileManager + FileManagerProtocol extension FileManager: FileManagerProtocol { - func append(_ data: Data, to url: URL) throws { + public func append(_ data: Data, to url: URL) throws { let handle = try FileHandle(forWritingTo: url) try handle.seekToEnd() try handle.write(contentsOf: data) try handle.close() } - func createDirectory(at url: URL, withIntermediateDirectories: Bool) throws { + public func createDirectory(at url: URL, withIntermediateDirectories: Bool) throws { try createDirectory(at: url, withIntermediateDirectories: withIntermediateDirectories, attributes: nil) } - func setIsExcludedFromBackup(_ value: Bool, to url: URL) throws { + public func setIsExcludedFromBackup(_ value: Bool, to url: URL) throws { try url.setIsExcludedFromBackup(value) } - func write(_ data: Data, to url: URL) throws { + public func write(_ data: Data, to url: URL) throws { try data.write(to: url) } } diff --git a/BitwardenShared/Core/Platform/Utilities/FileManagerTests.swift b/BitwardenKit/Core/Platform/Utilities/FileManagerTests.swift similarity index 98% rename from BitwardenShared/Core/Platform/Utilities/FileManagerTests.swift rename to BitwardenKit/Core/Platform/Utilities/FileManagerTests.swift index 20079fec4..a124778f9 100644 --- a/BitwardenShared/Core/Platform/Utilities/FileManagerTests.swift +++ b/BitwardenKit/Core/Platform/Utilities/FileManagerTests.swift @@ -1,6 +1,6 @@ import XCTest -@testable import BitwardenShared +@testable import BitwardenKit class FileManagerTests: BitwardenTestCase { // MARK: Properties diff --git a/BitwardenKit/Core/Platform/Utilities/Mocks/MockFileManager.swift b/BitwardenKit/Core/Platform/Utilities/Mocks/MockFileManager.swift new file mode 100644 index 000000000..9b4e209b9 --- /dev/null +++ b/BitwardenKit/Core/Platform/Utilities/Mocks/MockFileManager.swift @@ -0,0 +1,63 @@ +import Foundation + +@testable import BitwardenKit + +public class MockFileManager: FileManagerProtocol { + public var appendDataData: Data? + public var appendDataResult: Result = .success(()) + public var appendDataURL: URL? + + public var attributesOfItemPath: String? + public var attributesOfItemResult: Result<[FileAttributeKey: Any], Error> = .success([:]) + + public var createDirectoryURL: URL? + public var createDirectoryCreateIntermediates: Bool? + public var createDirectoryResult: Result = .success(()) + + public var removeItemURLs = [URL]() + public var removeItemResult: Result = .success(()) + + public var setIsExcludedFromBackupValue: Bool? + public var setIsExcludedFromBackupURL: URL? + public var setIsExcludedFromBackupResult: Result = .success(()) + + public var writeDataData: Data? + public var writeDataURL: URL? + public var writeDataResult: Result = .success(()) + + public init() {} + + public func append(_ data: Data, to url: URL) throws { + appendDataData = data + appendDataURL = url + try appendDataResult.get() + } + + public func attributesOfItem(atPath path: String) throws -> [FileAttributeKey: Any] { + attributesOfItemPath = path + return try attributesOfItemResult.get() + } + + public func createDirectory(at url: URL, withIntermediateDirectories createIntermediates: Bool) throws { + createDirectoryURL = url + createDirectoryCreateIntermediates = createIntermediates + try createDirectoryResult.get() + } + + public func removeItem(at url: URL) throws { + removeItemURLs.append(url) + try removeItemResult.get() + } + + public func setIsExcludedFromBackup(_ value: Bool, to url: URL) throws { + setIsExcludedFromBackupValue = value + setIsExcludedFromBackupURL = url + try setIsExcludedFromBackupResult.get() + } + + public func write(_ data: Data, to url: URL) throws { + writeDataData = data + writeDataURL = url + try writeDataResult.get() + } +} diff --git a/BitwardenKit/UI/Platform/Settings/FlightRecorder/Alert+FlightRecorder.swift b/BitwardenKit/UI/Platform/Settings/FlightRecorder/Alert+FlightRecorder.swift new file mode 100644 index 000000000..077e35f02 --- /dev/null +++ b/BitwardenKit/UI/Platform/Settings/FlightRecorder/Alert+FlightRecorder.swift @@ -0,0 +1,27 @@ +import BitwardenResources + +// MARK: - Alert + Settings + +extension Alert { + // MARK: Methods + + /// Confirm deleting a flight recorder log. + /// + /// - Parameters: + /// - isBulkDeletion: Whether the user is attempting to delete all logs or just a single log. + /// - action: The action to perform if the user selects yes to confirm deletion. + /// - Returns: An alert to confirm deleting a flight recorder log. + /// + static func confirmDeleteLog(isBulkDeletion: Bool, action: @MainActor @escaping () async -> Void) -> Alert { + Alert( + title: isBulkDeletion + ? Localizations.doYouReallyWantToDeleteAllRecordedLogs + : Localizations.doYouReallyWantToDeleteThisLog, + message: nil, + alertActions: [ + AlertAction(title: Localizations.yes, style: .default) { _ in await action() }, + AlertAction(title: Localizations.cancel, style: .cancel), + ], + ) + } +} diff --git a/BitwardenKit/UI/Platform/Settings/FlightRecorder/AlertFlightRecorderTests.swift b/BitwardenKit/UI/Platform/Settings/FlightRecorder/AlertFlightRecorderTests.swift new file mode 100644 index 000000000..b3a130c2f --- /dev/null +++ b/BitwardenKit/UI/Platform/Settings/FlightRecorder/AlertFlightRecorderTests.swift @@ -0,0 +1,44 @@ +import BitwardenResources +import XCTest + +@testable import BitwardenKit + +class AlertFlightRecorderTests: BitwardenTestCase { + // MARK: Tests + + /// `confirmDeleteLog(action:)` constructs an `Alert` with the title, + /// message, yes, and cancel buttons to confirm deleting a log. + func test_confirmDeleteLog() async throws { + var actionCalled = false + let subject = Alert.confirmDeleteLog(isBulkDeletion: false) { actionCalled = true } + + XCTAssertEqual(subject.alertActions.count, 2) + XCTAssertEqual(subject.preferredStyle, .alert) + XCTAssertEqual(subject.title, Localizations.doYouReallyWantToDeleteThisLog) + XCTAssertNil(subject.message) + + try await subject.tapAction(title: Localizations.cancel) + XCTAssertFalse(actionCalled) + + try await subject.tapAction(title: Localizations.yes) + XCTAssertTrue(actionCalled) + } + + /// `confirmDeleteLog(action:)` constructs an `Alert` with the title, + /// message, yes, and cancel buttons to confirm deleting all logs. + func test_confirmDeleteLog_bulkDeletion() async throws { + var actionCalled = false + let subject = Alert.confirmDeleteLog(isBulkDeletion: true) { actionCalled = true } + + XCTAssertEqual(subject.alertActions.count, 2) + XCTAssertEqual(subject.preferredStyle, .alert) + XCTAssertEqual(subject.title, Localizations.doYouReallyWantToDeleteAllRecordedLogs) + XCTAssertNil(subject.message) + + try await subject.tapAction(title: Localizations.cancel) + XCTAssertFalse(actionCalled) + + try await subject.tapAction(title: Localizations.yes) + XCTAssertTrue(actionCalled) + } +} diff --git a/BitwardenShared/UI/Platform/Settings/Settings/About/EnableFlightRecorder/EnableFlightRecorderAction.swift b/BitwardenKit/UI/Platform/Settings/FlightRecorder/EnableFlightRecorder/EnableFlightRecorderAction.swift similarity index 100% rename from BitwardenShared/UI/Platform/Settings/Settings/About/EnableFlightRecorder/EnableFlightRecorderAction.swift rename to BitwardenKit/UI/Platform/Settings/FlightRecorder/EnableFlightRecorder/EnableFlightRecorderAction.swift diff --git a/BitwardenShared/UI/Platform/Settings/Settings/About/EnableFlightRecorder/EnableFlightRecorderEffect.swift b/BitwardenKit/UI/Platform/Settings/FlightRecorder/EnableFlightRecorder/EnableFlightRecorderEffect.swift similarity index 100% rename from BitwardenShared/UI/Platform/Settings/Settings/About/EnableFlightRecorder/EnableFlightRecorderEffect.swift rename to BitwardenKit/UI/Platform/Settings/FlightRecorder/EnableFlightRecorder/EnableFlightRecorderEffect.swift diff --git a/BitwardenShared/UI/Platform/Settings/Settings/About/EnableFlightRecorder/EnableFlightRecorderProcessor.swift b/BitwardenKit/UI/Platform/Settings/FlightRecorder/EnableFlightRecorder/EnableFlightRecorderProcessor.swift similarity index 92% rename from BitwardenShared/UI/Platform/Settings/Settings/About/EnableFlightRecorder/EnableFlightRecorderProcessor.swift rename to BitwardenKit/UI/Platform/Settings/FlightRecorder/EnableFlightRecorder/EnableFlightRecorderProcessor.swift index 8c8db8e7a..f20465c8e 100644 --- a/BitwardenShared/UI/Platform/Settings/Settings/About/EnableFlightRecorder/EnableFlightRecorderProcessor.swift +++ b/BitwardenKit/UI/Platform/Settings/FlightRecorder/EnableFlightRecorder/EnableFlightRecorderProcessor.swift @@ -1,4 +1,3 @@ -import BitwardenKit import Foundation // MARK: - EnableFlightRecorderProcessor @@ -18,7 +17,7 @@ final class EnableFlightRecorderProcessor: StateProcessor< // MARK: Private Properties /// The `Coordinator` that handles navigation. - private let coordinator: AnyCoordinator + private let coordinator: AnyCoordinator /// The services used by this processor. private let services: Services @@ -33,7 +32,7 @@ final class EnableFlightRecorderProcessor: StateProcessor< /// - state: The initial state of the processor. /// init( - coordinator: AnyCoordinator, + coordinator: AnyCoordinator, services: Services, state: EnableFlightRecorderState, ) { diff --git a/BitwardenShared/UI/Platform/Settings/Settings/About/EnableFlightRecorder/EnableFlightRecorderProcessorTests.swift b/BitwardenKit/UI/Platform/Settings/FlightRecorder/EnableFlightRecorder/EnableFlightRecorderProcessorTests.swift similarity index 96% rename from BitwardenShared/UI/Platform/Settings/Settings/About/EnableFlightRecorder/EnableFlightRecorderProcessorTests.swift rename to BitwardenKit/UI/Platform/Settings/FlightRecorder/EnableFlightRecorder/EnableFlightRecorderProcessorTests.swift index 009c6558d..18950c4f8 100644 --- a/BitwardenShared/UI/Platform/Settings/Settings/About/EnableFlightRecorder/EnableFlightRecorderProcessorTests.swift +++ b/BitwardenKit/UI/Platform/Settings/FlightRecorder/EnableFlightRecorder/EnableFlightRecorderProcessorTests.swift @@ -2,12 +2,12 @@ import BitwardenKitMocks import TestHelpers import XCTest -@testable import BitwardenShared +@testable import BitwardenKit class EnableFlightRecorderProcessorTests: BitwardenTestCase { // MARK: Properties - var coordinator: MockCoordinator! + var coordinator: MockCoordinator! var errorReporter: MockErrorReporter! var flightRecorder: MockFlightRecorder! var subject: EnableFlightRecorderProcessor! diff --git a/BitwardenShared/UI/Platform/Settings/Settings/About/EnableFlightRecorder/EnableFlightRecorderState.swift b/BitwardenKit/UI/Platform/Settings/FlightRecorder/EnableFlightRecorder/EnableFlightRecorderState.swift similarity index 100% rename from BitwardenShared/UI/Platform/Settings/Settings/About/EnableFlightRecorder/EnableFlightRecorderState.swift rename to BitwardenKit/UI/Platform/Settings/FlightRecorder/EnableFlightRecorder/EnableFlightRecorderState.swift diff --git a/BitwardenShared/UI/Platform/Settings/Settings/About/EnableFlightRecorder/EnableFlightRecorderView+SnapshotTests.swift b/BitwardenKit/UI/Platform/Settings/FlightRecorder/EnableFlightRecorder/EnableFlightRecorderView+SnapshotTests.swift similarity index 95% rename from BitwardenShared/UI/Platform/Settings/Settings/About/EnableFlightRecorder/EnableFlightRecorderView+SnapshotTests.swift rename to BitwardenKit/UI/Platform/Settings/FlightRecorder/EnableFlightRecorder/EnableFlightRecorderView+SnapshotTests.swift index ffba1418e..eadbce7bc 100644 --- a/BitwardenShared/UI/Platform/Settings/Settings/About/EnableFlightRecorder/EnableFlightRecorderView+SnapshotTests.swift +++ b/BitwardenKit/UI/Platform/Settings/FlightRecorder/EnableFlightRecorder/EnableFlightRecorderView+SnapshotTests.swift @@ -1,11 +1,10 @@ // swiftlint:disable:this file_name -import BitwardenKit import BitwardenKitMocks import BitwardenResources import SnapshotTesting import XCTest -@testable import BitwardenShared +@testable import BitwardenKit class EnableFlightRecorderViewTests: BitwardenTestCase { // MARK: Properties diff --git a/BitwardenShared/UI/Platform/Settings/Settings/About/EnableFlightRecorder/EnableFlightRecorderView+ViewInspectorTests.swift b/BitwardenKit/UI/Platform/Settings/FlightRecorder/EnableFlightRecorder/EnableFlightRecorderView+ViewInspectorTests.swift similarity index 97% rename from BitwardenShared/UI/Platform/Settings/Settings/About/EnableFlightRecorder/EnableFlightRecorderView+ViewInspectorTests.swift rename to BitwardenKit/UI/Platform/Settings/FlightRecorder/EnableFlightRecorder/EnableFlightRecorderView+ViewInspectorTests.swift index 2ee2f2833..88f68b130 100644 --- a/BitwardenShared/UI/Platform/Settings/Settings/About/EnableFlightRecorder/EnableFlightRecorderView+ViewInspectorTests.swift +++ b/BitwardenKit/UI/Platform/Settings/FlightRecorder/EnableFlightRecorder/EnableFlightRecorderView+ViewInspectorTests.swift @@ -1,11 +1,10 @@ // swiftlint:disable:this file_name -import BitwardenKit import BitwardenKitMocks import BitwardenResources import ViewInspectorTestHelpers import XCTest -@testable import BitwardenShared +@testable import BitwardenKit class EnableFlightRecorderViewTests: BitwardenTestCase { // MARK: Properties diff --git a/BitwardenShared/UI/Platform/Settings/Settings/About/EnableFlightRecorder/EnableFlightRecorderView.swift b/BitwardenKit/UI/Platform/Settings/FlightRecorder/EnableFlightRecorder/EnableFlightRecorderView.swift similarity index 99% rename from BitwardenShared/UI/Platform/Settings/Settings/About/EnableFlightRecorder/EnableFlightRecorderView.swift rename to BitwardenKit/UI/Platform/Settings/FlightRecorder/EnableFlightRecorder/EnableFlightRecorderView.swift index 06e7afd3d..fb5c4ee4f 100644 --- a/BitwardenShared/UI/Platform/Settings/Settings/About/EnableFlightRecorder/EnableFlightRecorderView.swift +++ b/BitwardenKit/UI/Platform/Settings/FlightRecorder/EnableFlightRecorder/EnableFlightRecorderView.swift @@ -1,4 +1,3 @@ -import BitwardenKit import BitwardenResources import SwiftUI diff --git a/BitwardenShared/UI/Platform/Settings/Settings/About/EnableFlightRecorder/__Snapshots__/EnableFlightRecorderViewTests/test_snapshot_enableFlightRecorder.1.png b/BitwardenKit/UI/Platform/Settings/FlightRecorder/EnableFlightRecorder/__Snapshots__/EnableFlightRecorderViewTests/test_snapshot_enableFlightRecorder.1.png similarity index 100% rename from BitwardenShared/UI/Platform/Settings/Settings/About/EnableFlightRecorder/__Snapshots__/EnableFlightRecorderViewTests/test_snapshot_enableFlightRecorder.1.png rename to BitwardenKit/UI/Platform/Settings/FlightRecorder/EnableFlightRecorder/__Snapshots__/EnableFlightRecorderViewTests/test_snapshot_enableFlightRecorder.1.png diff --git a/BitwardenShared/UI/Platform/Settings/Settings/About/EnableFlightRecorder/__Snapshots__/EnableFlightRecorderViewTests/test_snapshot_enableFlightRecorder.2.png b/BitwardenKit/UI/Platform/Settings/FlightRecorder/EnableFlightRecorder/__Snapshots__/EnableFlightRecorderViewTests/test_snapshot_enableFlightRecorder.2.png similarity index 100% rename from BitwardenShared/UI/Platform/Settings/Settings/About/EnableFlightRecorder/__Snapshots__/EnableFlightRecorderViewTests/test_snapshot_enableFlightRecorder.2.png rename to BitwardenKit/UI/Platform/Settings/FlightRecorder/EnableFlightRecorder/__Snapshots__/EnableFlightRecorderViewTests/test_snapshot_enableFlightRecorder.2.png diff --git a/BitwardenShared/UI/Platform/Settings/Settings/About/EnableFlightRecorder/__Snapshots__/EnableFlightRecorderViewTests/test_snapshot_enableFlightRecorder.3.png b/BitwardenKit/UI/Platform/Settings/FlightRecorder/EnableFlightRecorder/__Snapshots__/EnableFlightRecorderViewTests/test_snapshot_enableFlightRecorder.3.png similarity index 100% rename from BitwardenShared/UI/Platform/Settings/Settings/About/EnableFlightRecorder/__Snapshots__/EnableFlightRecorderViewTests/test_snapshot_enableFlightRecorder.3.png rename to BitwardenKit/UI/Platform/Settings/FlightRecorder/EnableFlightRecorder/__Snapshots__/EnableFlightRecorderViewTests/test_snapshot_enableFlightRecorder.3.png diff --git a/BitwardenKit/UI/Platform/Settings/FlightRecorder/FlightRecorderCoordinator.swift b/BitwardenKit/UI/Platform/Settings/FlightRecorder/FlightRecorderCoordinator.swift new file mode 100644 index 000000000..2ac0c0e84 --- /dev/null +++ b/BitwardenKit/UI/Platform/Settings/FlightRecorder/FlightRecorderCoordinator.swift @@ -0,0 +1,97 @@ +import UIKit + +// MARK: - FlightRecorderCoordinator + +/// A coordinator that manages navigation for the flight recorder. +/// +public final class FlightRecorderCoordinator: Coordinator, HasStackNavigator { + // MARK: Types + + public typealias Services = HasErrorAlertServices.ErrorAlertServices + & HasErrorReporter + & HasFlightRecorder + & HasTimeProvider + + /// The services used by this coordinator. + private let services: Services + + // MARK: Properties + + /// The stack navigator that is managed by this coordinator. + public private(set) weak var stackNavigator: StackNavigator? + + // MARK: Initialization + + /// Creates a new `FlightRecorderCoordinator`. + /// + /// - Parameters: + /// - services: The services used by this coordinator. + /// - stackNavigator: The stack navigator that is managed by this coordinator. + /// + public init( + services: Services, + stackNavigator: StackNavigator, + ) { + self.services = services + self.stackNavigator = stackNavigator + } + + // MARK: Methods + + public func navigate(to route: FlightRecorderRoute, context: AnyObject?) { + switch route { + case .dismiss: + stackNavigator?.dismiss() + case .enableFlightRecorder: + showEnableFlightRecorder() + case .flightRecorderLogs: + showFlightRecorderLogs() + case let .shareURL(url): + showShareSheet([url]) + case let .shareURLs(urls): + showShareSheet(urls) + } + } + + public func start() {} + + // MARK: Private + + /// Shows the enable flight recorder screen. + /// + private func showEnableFlightRecorder() { + let processor = EnableFlightRecorderProcessor( + coordinator: asAnyCoordinator(), + services: services, + state: EnableFlightRecorderState(), + ) + stackNavigator?.present(EnableFlightRecorderView(store: Store(processor: processor))) + } + + /// Shows the flight recorder logs screen. + /// + private func showFlightRecorderLogs() { + let processor = FlightRecorderLogsProcessor( + coordinator: asAnyCoordinator(), + services: services, + state: FlightRecorderLogsState(), + ) + let view = FlightRecorderLogsView(store: Store(processor: processor), timeProvider: services.timeProvider) + stackNavigator?.present(view) + } + + /// Shows the share sheet to share one or more items. + /// + /// - Parameter items: The items to share. + /// + private func showShareSheet(_ items: [Any]) { + let activityVC = UIActivityViewController(activityItems: items, applicationActivities: nil) + stackNavigator?.present(activityVC) + } +} + +// MARK: - HasErrorAlertServices + +extension FlightRecorderCoordinator: HasErrorAlertServices { + public var errorAlertServices: ErrorAlertServices { services } +} diff --git a/BitwardenKit/UI/Platform/Settings/FlightRecorder/FlightRecorderCoordinatorTests.swift b/BitwardenKit/UI/Platform/Settings/FlightRecorder/FlightRecorderCoordinatorTests.swift new file mode 100644 index 000000000..20bf0a4a7 --- /dev/null +++ b/BitwardenKit/UI/Platform/Settings/FlightRecorder/FlightRecorderCoordinatorTests.swift @@ -0,0 +1,86 @@ +import BitwardenKitMocks +import SwiftUI +import XCTest + +@testable import BitwardenKit + +class FlightRecorderCoordinatorTests: BitwardenTestCase { + // MARK: Properties + + var stackNavigator: MockStackNavigator! + var subject: FlightRecorderCoordinator! + + // MARK: Setup & Teardown + + override func setUp() { + super.setUp() + + stackNavigator = MockStackNavigator() + + subject = FlightRecorderCoordinator( + services: ServiceContainer.withMocks(), + stackNavigator: stackNavigator, + ) + } + + override func tearDown() { + super.tearDown() + + stackNavigator = nil + subject = nil + } + + // MARK: Tests + + /// `navigate(to:)` with `.dismiss` dismisses the top most view presented by the stack + /// navigator. + @MainActor + func test_navigate_dismiss() throws { + subject.navigate(to: .dismiss) + + let action = try XCTUnwrap(stackNavigator.actions.last) + XCTAssertEqual(action.type, .dismissed) + } + + /// `navigate(to:)` with `.enableFlightRecorder` presents the enable flight recorder view. + @MainActor + func test_navigateTo_enableFlightRecorder() throws { + subject.navigate(to: .enableFlightRecorder) + + let action = try XCTUnwrap(stackNavigator.actions.last) + XCTAssertEqual(action.type, .presented) + XCTAssertTrue(action.view is EnableFlightRecorderView) + XCTAssertEqual(action.embedInNavigationController, true) + } + + /// `navigate(to:)` with `.flightRecorderLogs` presents the flight recorder logs view. + @MainActor + func test_navigateTo_flightRecorderLogs() throws { + subject.navigate(to: .flightRecorderLogs) + + let action = try XCTUnwrap(stackNavigator.actions.last) + XCTAssertEqual(action.type, .presented) + XCTAssertTrue(action.view is FlightRecorderLogsView) + XCTAssertEqual(action.embedInNavigationController, true) + } + + /// `navigate(to:)` with `.shareURL(_:)` presents an activity view controller to share the URL. + @MainActor + func test_navigateTo_shareURL() throws { + subject.navigate(to: .shareURL(.example)) + + let action = try XCTUnwrap(stackNavigator.actions.last) + XCTAssertEqual(action.type, .presented) + XCTAssertTrue(action.view is UIActivityViewController) + } + + /// `navigate(to:)` with `.shareURL(_:)` presents an activity view controller to share the URLs. + @MainActor + func test_navigateTo_shareURLs() throws { + subject.navigate(to: .shareURLs([.example])) + + let action = try XCTUnwrap(stackNavigator.actions.last) + XCTAssertEqual(action.type, .presented) + XCTAssertTrue(action.view is UIActivityViewController) + } +} diff --git a/BitwardenShared/UI/Platform/Settings/Settings/About/FlightRecorderLogs/FlightRecorderLogsAction.swift b/BitwardenKit/UI/Platform/Settings/FlightRecorder/FlightRecorderLogs/FlightRecorderLogsAction.swift similarity index 96% rename from BitwardenShared/UI/Platform/Settings/Settings/About/FlightRecorderLogs/FlightRecorderLogsAction.swift rename to BitwardenKit/UI/Platform/Settings/FlightRecorder/FlightRecorderLogs/FlightRecorderLogsAction.swift index ede6248f4..8197000aa 100644 --- a/BitwardenShared/UI/Platform/Settings/Settings/About/FlightRecorderLogs/FlightRecorderLogsAction.swift +++ b/BitwardenKit/UI/Platform/Settings/FlightRecorder/FlightRecorderLogs/FlightRecorderLogsAction.swift @@ -1,5 +1,3 @@ -import BitwardenKit - // MARK: - FlightRecorderLogsAction /// Actions handled by the `FlightRecorderLogsProcessor`. diff --git a/BitwardenShared/UI/Platform/Settings/Settings/About/FlightRecorderLogs/FlightRecorderLogsEffect.swift b/BitwardenKit/UI/Platform/Settings/FlightRecorder/FlightRecorderLogs/FlightRecorderLogsEffect.swift similarity index 100% rename from BitwardenShared/UI/Platform/Settings/Settings/About/FlightRecorderLogs/FlightRecorderLogsEffect.swift rename to BitwardenKit/UI/Platform/Settings/FlightRecorder/FlightRecorderLogs/FlightRecorderLogsEffect.swift diff --git a/BitwardenShared/UI/Platform/Settings/Settings/About/FlightRecorderLogs/FlightRecorderLogsProcessor.swift b/BitwardenKit/UI/Platform/Settings/FlightRecorder/FlightRecorderLogs/FlightRecorderLogsProcessor.swift similarity index 96% rename from BitwardenShared/UI/Platform/Settings/Settings/About/FlightRecorderLogs/FlightRecorderLogsProcessor.swift rename to BitwardenKit/UI/Platform/Settings/FlightRecorder/FlightRecorderLogs/FlightRecorderLogsProcessor.swift index 65d28fab0..9d4427fd8 100644 --- a/BitwardenShared/UI/Platform/Settings/Settings/About/FlightRecorderLogs/FlightRecorderLogsProcessor.swift +++ b/BitwardenKit/UI/Platform/Settings/FlightRecorder/FlightRecorderLogs/FlightRecorderLogsProcessor.swift @@ -1,4 +1,3 @@ -import BitwardenKit import BitwardenResources import Foundation @@ -19,7 +18,7 @@ final class FlightRecorderLogsProcessor: StateProcessor< // MARK: Private Properties /// The `Coordinator` that handles navigation. - private let coordinator: AnyCoordinator + private let coordinator: AnyCoordinator /// The services used by this processor. private let services: Services @@ -34,7 +33,7 @@ final class FlightRecorderLogsProcessor: StateProcessor< /// - state: The initial state of the processor. /// init( - coordinator: AnyCoordinator, + coordinator: AnyCoordinator, services: Services, state: FlightRecorderLogsState, ) { diff --git a/BitwardenShared/UI/Platform/Settings/Settings/About/FlightRecorderLogs/FlightRecorderLogsProcessorTests.swift b/BitwardenKit/UI/Platform/Settings/FlightRecorder/FlightRecorderLogs/FlightRecorderLogsProcessorTests.swift similarity index 98% rename from BitwardenShared/UI/Platform/Settings/Settings/About/FlightRecorderLogs/FlightRecorderLogsProcessorTests.swift rename to BitwardenKit/UI/Platform/Settings/FlightRecorder/FlightRecorderLogs/FlightRecorderLogsProcessorTests.swift index dc528e4b1..bbfd8dea4 100644 --- a/BitwardenShared/UI/Platform/Settings/Settings/About/FlightRecorderLogs/FlightRecorderLogsProcessorTests.swift +++ b/BitwardenKit/UI/Platform/Settings/FlightRecorder/FlightRecorderLogs/FlightRecorderLogsProcessorTests.swift @@ -1,15 +1,14 @@ -import BitwardenKit import BitwardenKitMocks import BitwardenResources import TestHelpers import XCTest -@testable import BitwardenShared +@testable import BitwardenKit class FlightRecorderLogsProcessorTests: BitwardenTestCase { // MARK: Properties - var coordinator: MockCoordinator! + var coordinator: MockCoordinator! var errorReporter: MockErrorReporter! var flightRecorder: MockFlightRecorder! var subject: FlightRecorderLogsProcessor! diff --git a/BitwardenShared/UI/Platform/Settings/Settings/About/FlightRecorderLogs/FlightRecorderLogsState.swift b/BitwardenKit/UI/Platform/Settings/FlightRecorder/FlightRecorderLogs/FlightRecorderLogsState.swift similarity index 96% rename from BitwardenShared/UI/Platform/Settings/Settings/About/FlightRecorderLogs/FlightRecorderLogsState.swift rename to BitwardenKit/UI/Platform/Settings/FlightRecorder/FlightRecorderLogs/FlightRecorderLogsState.swift index 8ec841ce7..f4599ffe6 100644 --- a/BitwardenShared/UI/Platform/Settings/Settings/About/FlightRecorderLogs/FlightRecorderLogsState.swift +++ b/BitwardenKit/UI/Platform/Settings/FlightRecorder/FlightRecorderLogs/FlightRecorderLogsState.swift @@ -1,5 +1,3 @@ -import BitwardenKit - // MARK: - FlightRecorderLogsState /// An object that defines the current state of the `FlightRecorderLogsView`. diff --git a/BitwardenShared/UI/Platform/Settings/Settings/About/FlightRecorderLogs/FlightRecorderLogsView+SnapshotTests.swift b/BitwardenKit/UI/Platform/Settings/FlightRecorder/FlightRecorderLogs/FlightRecorderLogsView+SnapshotTests.swift similarity index 98% rename from BitwardenShared/UI/Platform/Settings/Settings/About/FlightRecorderLogs/FlightRecorderLogsView+SnapshotTests.swift rename to BitwardenKit/UI/Platform/Settings/FlightRecorder/FlightRecorderLogs/FlightRecorderLogsView+SnapshotTests.swift index 1dcd9678b..0c8b197db 100644 --- a/BitwardenShared/UI/Platform/Settings/Settings/About/FlightRecorderLogs/FlightRecorderLogsView+SnapshotTests.swift +++ b/BitwardenKit/UI/Platform/Settings/FlightRecorder/FlightRecorderLogs/FlightRecorderLogsView+SnapshotTests.swift @@ -1,11 +1,10 @@ // swiftlint:disable:this file_name -import BitwardenKit import BitwardenKitMocks import BitwardenResources import SnapshotTesting import XCTest -@testable import BitwardenShared +@testable import BitwardenKit class FlightRecorderLogsViewTests: BitwardenTestCase { // MARK: Properties diff --git a/BitwardenShared/UI/Platform/Settings/Settings/About/FlightRecorderLogs/FlightRecorderLogsView+ViewInspectorTests.swift b/BitwardenKit/UI/Platform/Settings/FlightRecorder/FlightRecorderLogs/FlightRecorderLogsView+ViewInspectorTests.swift similarity index 98% rename from BitwardenShared/UI/Platform/Settings/Settings/About/FlightRecorderLogs/FlightRecorderLogsView+ViewInspectorTests.swift rename to BitwardenKit/UI/Platform/Settings/FlightRecorder/FlightRecorderLogs/FlightRecorderLogsView+ViewInspectorTests.swift index 99781430d..f89c6f22d 100644 --- a/BitwardenShared/UI/Platform/Settings/Settings/About/FlightRecorderLogs/FlightRecorderLogsView+ViewInspectorTests.swift +++ b/BitwardenKit/UI/Platform/Settings/FlightRecorder/FlightRecorderLogs/FlightRecorderLogsView+ViewInspectorTests.swift @@ -1,10 +1,10 @@ // swiftlint:disable:this file_name -import BitwardenKit import BitwardenKitMocks import BitwardenResources +import TestHelpers import XCTest -@testable import BitwardenShared +@testable import BitwardenKit class FlightRecorderLogsViewTests: BitwardenTestCase { // MARK: Properties diff --git a/BitwardenShared/UI/Platform/Settings/Settings/About/FlightRecorderLogs/FlightRecorderLogsView.swift b/BitwardenKit/UI/Platform/Settings/FlightRecorder/FlightRecorderLogs/FlightRecorderLogsView.swift similarity index 98% rename from BitwardenShared/UI/Platform/Settings/Settings/About/FlightRecorderLogs/FlightRecorderLogsView.swift rename to BitwardenKit/UI/Platform/Settings/FlightRecorder/FlightRecorderLogs/FlightRecorderLogsView.swift index 8cc106dd4..ce9aedc8d 100644 --- a/BitwardenShared/UI/Platform/Settings/Settings/About/FlightRecorderLogs/FlightRecorderLogsView.swift +++ b/BitwardenKit/UI/Platform/Settings/FlightRecorder/FlightRecorderLogs/FlightRecorderLogsView.swift @@ -1,4 +1,3 @@ -import BitwardenKit import BitwardenResources import SwiftUI @@ -53,7 +52,7 @@ struct FlightRecorderLogsView: View { logsList } else { IllustratedMessageView( - image: Asset.Images.Illustrations.secureDevices.swiftUIImage, + image: SharedAsset.Illustrations.secureDevices.swiftUIImage, style: .mediumImage, message: Localizations.noLogsRecorded, ) diff --git a/BitwardenShared/UI/Platform/Settings/Settings/About/FlightRecorderLogs/__Snapshots__/FlightRecorderLogsViewTests/test_snapshot_flightRecorderLogs_empty.1.png b/BitwardenKit/UI/Platform/Settings/FlightRecorder/FlightRecorderLogs/__Snapshots__/FlightRecorderLogsViewTests/test_snapshot_flightRecorderLogs_empty.1.png similarity index 100% rename from BitwardenShared/UI/Platform/Settings/Settings/About/FlightRecorderLogs/__Snapshots__/FlightRecorderLogsViewTests/test_snapshot_flightRecorderLogs_empty.1.png rename to BitwardenKit/UI/Platform/Settings/FlightRecorder/FlightRecorderLogs/__Snapshots__/FlightRecorderLogsViewTests/test_snapshot_flightRecorderLogs_empty.1.png diff --git a/BitwardenShared/UI/Platform/Settings/Settings/About/FlightRecorderLogs/__Snapshots__/FlightRecorderLogsViewTests/test_snapshot_flightRecorderLogs_empty.2.png b/BitwardenKit/UI/Platform/Settings/FlightRecorder/FlightRecorderLogs/__Snapshots__/FlightRecorderLogsViewTests/test_snapshot_flightRecorderLogs_empty.2.png similarity index 100% rename from BitwardenShared/UI/Platform/Settings/Settings/About/FlightRecorderLogs/__Snapshots__/FlightRecorderLogsViewTests/test_snapshot_flightRecorderLogs_empty.2.png rename to BitwardenKit/UI/Platform/Settings/FlightRecorder/FlightRecorderLogs/__Snapshots__/FlightRecorderLogsViewTests/test_snapshot_flightRecorderLogs_empty.2.png diff --git a/BitwardenShared/UI/Platform/Settings/Settings/About/FlightRecorderLogs/__Snapshots__/FlightRecorderLogsViewTests/test_snapshot_flightRecorderLogs_empty.3.png b/BitwardenKit/UI/Platform/Settings/FlightRecorder/FlightRecorderLogs/__Snapshots__/FlightRecorderLogsViewTests/test_snapshot_flightRecorderLogs_empty.3.png similarity index 100% rename from BitwardenShared/UI/Platform/Settings/Settings/About/FlightRecorderLogs/__Snapshots__/FlightRecorderLogsViewTests/test_snapshot_flightRecorderLogs_empty.3.png rename to BitwardenKit/UI/Platform/Settings/FlightRecorder/FlightRecorderLogs/__Snapshots__/FlightRecorderLogsViewTests/test_snapshot_flightRecorderLogs_empty.3.png diff --git a/BitwardenShared/UI/Platform/Settings/Settings/About/FlightRecorderLogs/__Snapshots__/FlightRecorderLogsViewTests/test_snapshot_flightRecorderLogs_populated.1.png b/BitwardenKit/UI/Platform/Settings/FlightRecorder/FlightRecorderLogs/__Snapshots__/FlightRecorderLogsViewTests/test_snapshot_flightRecorderLogs_populated.1.png similarity index 100% rename from BitwardenShared/UI/Platform/Settings/Settings/About/FlightRecorderLogs/__Snapshots__/FlightRecorderLogsViewTests/test_snapshot_flightRecorderLogs_populated.1.png rename to BitwardenKit/UI/Platform/Settings/FlightRecorder/FlightRecorderLogs/__Snapshots__/FlightRecorderLogsViewTests/test_snapshot_flightRecorderLogs_populated.1.png diff --git a/BitwardenShared/UI/Platform/Settings/Settings/About/FlightRecorderLogs/__Snapshots__/FlightRecorderLogsViewTests/test_snapshot_flightRecorderLogs_populated.2.png b/BitwardenKit/UI/Platform/Settings/FlightRecorder/FlightRecorderLogs/__Snapshots__/FlightRecorderLogsViewTests/test_snapshot_flightRecorderLogs_populated.2.png similarity index 100% rename from BitwardenShared/UI/Platform/Settings/Settings/About/FlightRecorderLogs/__Snapshots__/FlightRecorderLogsViewTests/test_snapshot_flightRecorderLogs_populated.2.png rename to BitwardenKit/UI/Platform/Settings/FlightRecorder/FlightRecorderLogs/__Snapshots__/FlightRecorderLogsViewTests/test_snapshot_flightRecorderLogs_populated.2.png diff --git a/BitwardenShared/UI/Platform/Settings/Settings/About/FlightRecorderLogs/__Snapshots__/FlightRecorderLogsViewTests/test_snapshot_flightRecorderLogs_populated.3.png b/BitwardenKit/UI/Platform/Settings/FlightRecorder/FlightRecorderLogs/__Snapshots__/FlightRecorderLogsViewTests/test_snapshot_flightRecorderLogs_populated.3.png similarity index 100% rename from BitwardenShared/UI/Platform/Settings/Settings/About/FlightRecorderLogs/__Snapshots__/FlightRecorderLogsViewTests/test_snapshot_flightRecorderLogs_populated.3.png rename to BitwardenKit/UI/Platform/Settings/FlightRecorder/FlightRecorderLogs/__Snapshots__/FlightRecorderLogsViewTests/test_snapshot_flightRecorderLogs_populated.3.png diff --git a/BitwardenKit/UI/Platform/Settings/FlightRecorder/FlightRecorderModule.swift b/BitwardenKit/UI/Platform/Settings/FlightRecorder/FlightRecorderModule.swift new file mode 100644 index 000000000..688ebe72b --- /dev/null +++ b/BitwardenKit/UI/Platform/Settings/FlightRecorder/FlightRecorderModule.swift @@ -0,0 +1,16 @@ +// MARK: - FlightRecorderModule + +/// An object that builds coordinators for the flight recorder flow. +/// +@MainActor +public protocol FlightRecorderModule { + /// Initializes a coordinator for navigating between `FlightRecorderRoute`s. + /// + /// - Parameters: + /// - stackNavigator: The stack navigator that will be used to navigate between routes. + /// - Returns: A coordinator that can navigate to `FlightRecorderRoute`s. + /// + func makeFlightRecorderCoordinator( + stackNavigator: StackNavigator, + ) -> AnyCoordinator +} diff --git a/BitwardenKit/UI/Platform/Settings/FlightRecorder/FlightRecorderRoute.swift b/BitwardenKit/UI/Platform/Settings/FlightRecorder/FlightRecorderRoute.swift new file mode 100644 index 000000000..92bbd15ad --- /dev/null +++ b/BitwardenKit/UI/Platform/Settings/FlightRecorder/FlightRecorderRoute.swift @@ -0,0 +1,20 @@ +import Foundation + +/// A route to a specific screen within the flight recorder flow. +/// +public enum FlightRecorderRoute: Equatable, Hashable { + /// A route that dismisses the current view. + case dismiss + + /// A route to enable and configure flight recorder. + case enableFlightRecorder + + /// A route to the flight recorder logs screen. + case flightRecorderLogs + + /// A route to the share sheet to share a URL. + case shareURL(URL) + + /// A route to the share sheet to share multiple URLs. + case shareURLs([URL]) +} diff --git a/BitwardenResources/Illustrations.xcassets/Contents.json b/BitwardenResources/Illustrations.xcassets/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/BitwardenResources/Illustrations.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/BitwardenResources/Illustrations.xcassets/secure-devices.imageset/Contents.json b/BitwardenResources/Illustrations.xcassets/secure-devices.imageset/Contents.json new file mode 100644 index 000000000..27aa29450 --- /dev/null +++ b/BitwardenResources/Illustrations.xcassets/secure-devices.imageset/Contents.json @@ -0,0 +1,25 @@ +{ + "images" : [ + { + "filename" : "secure-devices.pdf", + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "secure-devices-dark.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/BitwardenResources/Illustrations.xcassets/secure-devices.imageset/secure-devices-dark.pdf b/BitwardenResources/Illustrations.xcassets/secure-devices.imageset/secure-devices-dark.pdf new file mode 100644 index 0000000000000000000000000000000000000000..089c477133aa59e9c04004c941249424ac37a38a GIT binary patch literal 3696 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 literal 0 HcmV?d00001 diff --git a/BitwardenResources/Illustrations.xcassets/secure-devices.imageset/secure-devices.pdf b/BitwardenResources/Illustrations.xcassets/secure-devices.imageset/secure-devices.pdf new file mode 100644 index 0000000000000000000000000000000000000000..d8c3f5d99b2cdce8f72747bf3febef637562a83b GIT binary patch literal 3691 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 URL { - containerURL(forSecurityApplicationGroupIdentifier: Bundle.main.groupIdentifier)! - .appendingPathComponent("FlightRecorderLogs", isDirectory: true) - } } diff --git a/BitwardenShared/Core/Platform/Extensions/Task+Extensions.swift b/BitwardenShared/Core/Platform/Extensions/Task+Extensions.swift deleted file mode 100644 index 8396b19e0..000000000 --- a/BitwardenShared/Core/Platform/Extensions/Task+Extensions.swift +++ /dev/null @@ -1,11 +0,0 @@ -import Foundation - -extension Task where Success == Never, Failure == Never { - static func sleep(forSeconds delay: Double, tolerance: Double = 1) async throws { - if #available(iOS 16.0, *) { - try await sleep(for: .seconds(delay), tolerance: .seconds(tolerance)) - } else { - try await sleep(nanoseconds: UInt64(delay * Double(NSEC_PER_SEC))) - } - } -} diff --git a/BitwardenShared/Core/Platform/Extensions/URL.swift b/BitwardenShared/Core/Platform/Extensions/URL.swift index a642abbb7..340802171 100644 --- a/BitwardenShared/Core/Platform/Extensions/URL.swift +++ b/BitwardenShared/Core/Platform/Extensions/URL.swift @@ -44,40 +44,4 @@ extension URL { var isApp: Bool { absoluteString.starts(with: Constants.iOSAppProtocol) } - - // MARK: Methods - - /// Creates a new `URL` appending the provided query items to the url. - /// - /// On iOS 16+, this method uses the method with the same name in Foundation. On iOS 15, this method - /// uses `URLComponents` to add the query items to the new url. - /// - /// - Parameter queryItems: A list of `URLQueryItem`s to add to this url. - /// - Returns: A new `URL` with the query items appended. - /// - func appending(queryItems: [URLQueryItem]) -> URL? { - if #available(iOS 16, *) { - // Set this variable to a non-optional `URL` type so that we are calling the function in Foundation, - // rather than recursively calling this method. - let url: URL = appending(queryItems: queryItems) - return url - } else { - guard var components = URLComponents(url: self, resolvingAgainstBaseURL: false) - else { return nil } - - components.queryItems = queryItems - return components.url - } - } - - /// Sets whether the file should be excluded from backups. - /// - /// - Parameter value: `true` if the file should be excluded from backups, or `false` otherwise. - /// - func setIsExcludedFromBackup(_ value: Bool) throws { - var url = self - var values = URLResourceValues() - values.isExcludedFromBackup = value - try url.setResourceValues(values) - } } diff --git a/BitwardenShared/Core/Platform/Services/ServiceContainer.swift b/BitwardenShared/Core/Platform/Services/ServiceContainer.swift index a56b30719..2a14556f4 100644 --- a/BitwardenShared/Core/Platform/Services/ServiceContainer.swift +++ b/BitwardenShared/Core/Platform/Services/ServiceContainer.swift @@ -98,7 +98,7 @@ public class ServiceContainer: Services { // swiftlint:disable:this type_body_le let fido2UserInterfaceHelper: Fido2UserInterfaceHelper /// The service used by the application for recording temporary debug logs. - let flightRecorder: FlightRecorder + public let flightRecorder: FlightRecorder /// The repository used by the application to manage generator data for the UI layer. let generatorRepository: GeneratorRepository diff --git a/BitwardenShared/Core/Platform/Services/Services.swift b/BitwardenShared/Core/Platform/Services/Services.swift index 4c6d243d6..75e9520ac 100644 --- a/BitwardenShared/Core/Platform/Services/Services.swift +++ b/BitwardenShared/Core/Platform/Services/Services.swift @@ -2,8 +2,6 @@ import AuthenticatorBridgeKit import BitwardenKit import BitwardenSdk -// swiftlint:disable file_length - /// The services provided by the `ServiceContainer`. typealias Services = HasAPIService & HasAccountAPIService @@ -209,13 +207,6 @@ protocol HasFileAPIService { var fileAPIService: FileAPIService { get } } -/// Protocol for an object that provides a `FlightRecorder`. -/// -protocol HasFlightRecorder { - /// The service used by the application for recording temporary debug logs. - var flightRecorder: FlightRecorder { get } -} - /// Protocol for an object that provides a `GeneratorRepository`. /// protocol HasGeneratorRepository { diff --git a/BitwardenShared/Core/Platform/Services/StateService.swift b/BitwardenShared/Core/Platform/Services/StateService.swift index 36ff5d830..ae2511d30 100644 --- a/BitwardenShared/Core/Platform/Services/StateService.swift +++ b/BitwardenShared/Core/Platform/Services/StateService.swift @@ -1437,7 +1437,7 @@ enum StateServiceError: LocalizedError { /// A default implementation of `StateService`. /// -actor DefaultStateService: StateService, ActiveAccountStateProvider, ConfigStateService { // swiftlint:disable:this type_body_length line_length +actor DefaultStateService: StateService, ActiveAccountStateProvider, ConfigStateService, FlightRecorderStateService { // swiftlint:disable:this type_body_length line_length // MARK: Properties /// The language option currently selected for the app. diff --git a/BitwardenShared/Core/Platform/Services/TestHelpers/MockFlightRecorder.swift b/BitwardenShared/Core/Platform/Services/TestHelpers/MockFlightRecorder.swift deleted file mode 100644 index 11ec96e2e..000000000 --- a/BitwardenShared/Core/Platform/Services/TestHelpers/MockFlightRecorder.swift +++ /dev/null @@ -1,64 +0,0 @@ -import Combine - -@testable import BitwardenShared - -@MainActor -final class MockFlightRecorder: FlightRecorder { - var activeLogSubject = CurrentValueSubject(nil) - var deleteInactiveLogsCalled = false - var deleteInactiveLogsResult: Result = .success(()) - var deleteLogResult: Result = .success(()) - var deleteLogLogs = [FlightRecorderLogMetadata]() - var disableFlightRecorderCalled = false - var enableFlightRecorderCalled = false - var enableFlightRecorderDuration: FlightRecorderLoggingDuration? - var enableFlightRecorderResult: Result = .success(()) - var fetchLogsCalled = false - var fetchLogsResult: Result<[FlightRecorderLogMetadata], Error> = .success([]) - var isEnabledSubject = CurrentValueSubject(false) - var logMessages = [String]() - var setFlightRecorderBannerDismissedCalled = false - - nonisolated init() {} - - func activeLogPublisher() async -> AnyPublisher { - activeLogSubject.eraseToAnyPublisher() - } - - func deleteInactiveLogs() async throws { - deleteInactiveLogsCalled = true - try deleteInactiveLogsResult.get() - } - - func deleteLog(_ log: FlightRecorderLogMetadata) async throws { - deleteLogLogs.append(log) - try deleteLogResult.get() - } - - func disableFlightRecorder() { - disableFlightRecorderCalled = true - } - - func enableFlightRecorder(duration: FlightRecorderLoggingDuration) async throws { - enableFlightRecorderCalled = true - enableFlightRecorderDuration = duration - try enableFlightRecorderResult.get() - } - - func fetchLogs() async throws -> [FlightRecorderLogMetadata] { - fetchLogsCalled = true - return try fetchLogsResult.get() - } - - func isEnabledPublisher() async -> AnyPublisher { - isEnabledSubject.eraseToAnyPublisher() - } - - func log(_ message: String, file: String, line: UInt) async { - logMessages.append(message) - } - - func setFlightRecorderBannerDismissed() async { - setFlightRecorderBannerDismissedCalled = true - } -} diff --git a/BitwardenShared/Core/Platform/Utilities/Constants.swift b/BitwardenShared/Core/Platform/Utilities/Constants.swift index 1e49c2e26..5c16ec680 100644 --- a/BitwardenShared/Core/Platform/Utilities/Constants.swift +++ b/BitwardenShared/Core/Platform/Utilities/Constants.swift @@ -26,10 +26,6 @@ extension Constants { /// The URL for the web vault if the user account doesn't have one specified. static let defaultWebVaultHost = "bitwarden.com" - /// The number of days that a flight recorder log will remain on the device after the end date - /// before being automatically deleted. - static let flightRecorderLogExpirationDays = 30 - /// The length of a masked password. static let hiddenPasswordLength = 8 diff --git a/BitwardenShared/Core/Platform/Utilities/TestHelpers/MockFileManager.swift b/BitwardenShared/Core/Platform/Utilities/TestHelpers/MockFileManager.swift deleted file mode 100644 index b59dc439d..000000000 --- a/BitwardenShared/Core/Platform/Utilities/TestHelpers/MockFileManager.swift +++ /dev/null @@ -1,61 +0,0 @@ -import Foundation - -@testable import BitwardenShared - -class MockFileManager: FileManagerProtocol { - var appendDataData: Data? - var appendDataResult: Result = .success(()) - var appendDataURL: URL? - - var attributesOfItemPath: String? - var attributesOfItemResult: Result<[FileAttributeKey: Any], Error> = .success([:]) - - var createDirectoryURL: URL? - var createDirectoryCreateIntermediates: Bool? - var createDirectoryResult: Result = .success(()) - - var removeItemURLs = [URL]() - var removeItemResult: Result = .success(()) - - var setIsExcludedFromBackupValue: Bool? - var setIsExcludedFromBackupURL: URL? - var setIsExcludedFromBackupResult: Result = .success(()) - - var writeDataData: Data? - var writeDataURL: URL? - var writeDataResult: Result = .success(()) - - func append(_ data: Data, to url: URL) throws { - appendDataData = data - appendDataURL = url - try appendDataResult.get() - } - - func attributesOfItem(atPath path: String) throws -> [FileAttributeKey: Any] { - attributesOfItemPath = path - return try attributesOfItemResult.get() - } - - func createDirectory(at url: URL, withIntermediateDirectories createIntermediates: Bool) throws { - createDirectoryURL = url - createDirectoryCreateIntermediates = createIntermediates - try createDirectoryResult.get() - } - - func removeItem(at url: URL) throws { - removeItemURLs.append(url) - try removeItemResult.get() - } - - func setIsExcludedFromBackup(_ value: Bool, to url: URL) throws { - setIsExcludedFromBackupValue = value - setIsExcludedFromBackupURL = url - try setIsExcludedFromBackupResult.get() - } - - func write(_ data: Data, to url: URL) throws { - writeDataData = data - writeDataURL = url - try writeDataResult.get() - } -} diff --git a/BitwardenShared/UI/Platform/Application/AppModule.swift b/BitwardenShared/UI/Platform/Application/AppModule.swift index e621c4197..4e3536503 100644 --- a/BitwardenShared/UI/Platform/Application/AppModule.swift +++ b/BitwardenShared/UI/Platform/Application/AppModule.swift @@ -62,3 +62,17 @@ extension DefaultAppModule: AppModule { ).asAnyCoordinator() } } + +// MARK: - DefaultAppModule + FlightRecorderModule + +extension DefaultAppModule: FlightRecorderModule { + public func makeFlightRecorderCoordinator( + stackNavigator: StackNavigator, + ) -> AnyCoordinator { + FlightRecorderCoordinator( + services: services, + stackNavigator: stackNavigator, + ) + .asAnyCoordinator() + } +} diff --git a/BitwardenShared/UI/Platform/Settings/Extensions/Alert+Settings.swift b/BitwardenShared/UI/Platform/Settings/Extensions/Alert+Settings.swift index f67fbaa4c..a412fc4f6 100644 --- a/BitwardenShared/UI/Platform/Settings/Extensions/Alert+Settings.swift +++ b/BitwardenShared/UI/Platform/Settings/Extensions/Alert+Settings.swift @@ -41,26 +41,6 @@ extension Alert { ) } - /// Confirm deleting a flight recorder log. - /// - /// - Parameters: - /// - isBulkDeletion: Whether the user is attempting to delete all logs or just a single log. - /// - action: The action to perform if the user selects yes to confirm deletion. - /// - Returns: An alert to confirm deleting a flight recorder log. - /// - static func confirmDeleteLog(isBulkDeletion: Bool, action: @MainActor @escaping () async -> Void) -> Alert { - Alert( - title: isBulkDeletion - ? Localizations.doYouReallyWantToDeleteAllRecordedLogs - : Localizations.doYouReallyWantToDeleteThisLog, - message: nil, - alertActions: [ - AlertAction(title: Localizations.yes, style: .default) { _ in await action() }, - AlertAction(title: Localizations.cancel, style: .cancel), - ], - ) - } - /// Confirm denying all the login requests. /// /// - Parameter action: The action to perform if the user selects yes. diff --git a/BitwardenShared/UI/Platform/Settings/Extensions/AlertSettingsTests.swift b/BitwardenShared/UI/Platform/Settings/Extensions/AlertSettingsTests.swift index f9cfb32c2..318457bf2 100644 --- a/BitwardenShared/UI/Platform/Settings/Extensions/AlertSettingsTests.swift +++ b/BitwardenShared/UI/Platform/Settings/Extensions/AlertSettingsTests.swift @@ -31,42 +31,6 @@ class AlertSettingsTests: BitwardenTestCase { XCTAssertNil(subject.message) } - /// `confirmDeleteLog(action:)` constructs an `Alert` with the title, - /// message, yes, and cancel buttons to confirm deleting a log. - func test_confirmDeleteLog() async throws { - var actionCalled = false - let subject = Alert.confirmDeleteLog(isBulkDeletion: false) { actionCalled = true } - - XCTAssertEqual(subject.alertActions.count, 2) - XCTAssertEqual(subject.preferredStyle, .alert) - XCTAssertEqual(subject.title, Localizations.doYouReallyWantToDeleteThisLog) - XCTAssertNil(subject.message) - - try await subject.tapAction(title: Localizations.cancel) - XCTAssertFalse(actionCalled) - - try await subject.tapAction(title: Localizations.yes) - XCTAssertTrue(actionCalled) - } - - /// `confirmDeleteLog(action:)` constructs an `Alert` with the title, - /// message, yes, and cancel buttons to confirm deleting all logs. - func test_confirmDeleteLog_bulkDeletion() async throws { - var actionCalled = false - let subject = Alert.confirmDeleteLog(isBulkDeletion: true) { actionCalled = true } - - XCTAssertEqual(subject.alertActions.count, 2) - XCTAssertEqual(subject.preferredStyle, .alert) - XCTAssertEqual(subject.title, Localizations.doYouReallyWantToDeleteAllRecordedLogs) - XCTAssertNil(subject.message) - - try await subject.tapAction(title: Localizations.cancel) - XCTAssertFalse(actionCalled) - - try await subject.tapAction(title: Localizations.yes) - XCTAssertTrue(actionCalled) - } - /// `confirmDenyingAllRequests(action:)` constructs an `Alert` with the title, /// message, yes, and cancel buttons to confirm denying all login requests func test_confirmDenyingAllRequests() { diff --git a/BitwardenShared/UI/Platform/Settings/Settings/About/AboutProcessor.swift b/BitwardenShared/UI/Platform/Settings/Settings/About/AboutProcessor.swift index 50c7a808f..c69adc6f2 100644 --- a/BitwardenShared/UI/Platform/Settings/Settings/About/AboutProcessor.swift +++ b/BitwardenShared/UI/Platform/Settings/Settings/About/AboutProcessor.swift @@ -57,7 +57,7 @@ final class AboutProcessor: StateProcessor await streamFlightRecorderLog() case let .toggleFlightRecorder(isOn): if isOn { - coordinator.navigate(to: .enableFlightRecorder) + coordinator.navigate(to: .flightRecorder(.enableFlightRecorder)) } else { await services.flightRecorder.disableFlightRecorder() } @@ -92,7 +92,7 @@ final class AboutProcessor: StateProcessor case .versionTapped: handleVersionTapped() case .viewFlightRecorderLogsTapped: - coordinator.navigate(to: .flightRecorderLogs) + coordinator.navigate(to: .flightRecorder(.flightRecorderLogs)) case .webVaultTapped: coordinator.showAlert(.webVaultAlert { self.state.url = self.services.environmentService.webVaultURL diff --git a/BitwardenShared/UI/Platform/Settings/Settings/About/AboutProcessorTests.swift b/BitwardenShared/UI/Platform/Settings/Settings/About/AboutProcessorTests.swift index b5ee2bbfd..819aceaff 100644 --- a/BitwardenShared/UI/Platform/Settings/Settings/About/AboutProcessorTests.swift +++ b/BitwardenShared/UI/Platform/Settings/Settings/About/AboutProcessorTests.swift @@ -122,7 +122,7 @@ class AboutProcessorTests: BitwardenTestCase { await subject.perform(.toggleFlightRecorder(true)) - XCTAssertEqual(coordinator.routes, [.enableFlightRecorder]) + XCTAssertEqual(coordinator.routes, [.flightRecorder(.enableFlightRecorder)]) } /// `receive(_:)` with `.clearAppReviewURL` clears the app review URL in the state. @@ -232,7 +232,7 @@ class AboutProcessorTests: BitwardenTestCase { func test_receive_viewFlightRecorderLogsTapped() { subject.receive(.viewFlightRecorderLogsTapped) - XCTAssertEqual(coordinator.routes, [.flightRecorderLogs]) + XCTAssertEqual(coordinator.routes, [.flightRecorder(.flightRecorderLogs)]) } /// `receive(_:)` with `.webVaultTapped` shows an alert for navigating to the web vault diff --git a/BitwardenShared/UI/Platform/Settings/Settings/About/FlightRecorderLogs/AboutStateTests.swift b/BitwardenShared/UI/Platform/Settings/Settings/About/AboutStateTests.swift similarity index 98% rename from BitwardenShared/UI/Platform/Settings/Settings/About/FlightRecorderLogs/AboutStateTests.swift rename to BitwardenShared/UI/Platform/Settings/Settings/About/AboutStateTests.swift index 80808797f..7a67bfb42 100644 --- a/BitwardenShared/UI/Platform/Settings/Settings/About/FlightRecorderLogs/AboutStateTests.swift +++ b/BitwardenShared/UI/Platform/Settings/Settings/About/AboutStateTests.swift @@ -1,3 +1,4 @@ +import BitwardenKit import BitwardenResources import XCTest diff --git a/BitwardenShared/UI/Platform/Settings/SettingsCoordinator.swift b/BitwardenShared/UI/Platform/Settings/SettingsCoordinator.swift index 264f14728..99f39ca1c 100644 --- a/BitwardenShared/UI/Platform/Settings/SettingsCoordinator.swift +++ b/BitwardenShared/UI/Platform/Settings/SettingsCoordinator.swift @@ -55,6 +55,7 @@ final class SettingsCoordinator: Coordinator, HasStackNavigator { // swiftlint:d typealias Module = AddEditFolderModule & AuthModule & ExportCXFModule + & FlightRecorderModule & ImportLoginsModule & LoginRequestModule & NavigatorBuilderModule @@ -152,8 +153,6 @@ final class SettingsCoordinator: Coordinator, HasStackNavigator { // swiftlint:d showAccountSecurity() case let .addEditFolder(folder): showAddEditFolder(folder, delegate: context as? AddEditFolderDelegate) - case .enableFlightRecorder: - showEnableFlightRecorder() case .appearance: showAppearance() case .appExtension: @@ -174,8 +173,8 @@ final class SettingsCoordinator: Coordinator, HasStackNavigator { // swiftlint:d showExportVaultToApp() case .exportVaultToFile: showExportVaultToFile() - case .flightRecorderLogs: - showFlightRecorderLogs() + case let .flightRecorder(route): + showFlightRecorder(route: route) case .folders: showFolders() case .importLogins: @@ -194,8 +193,6 @@ final class SettingsCoordinator: Coordinator, HasStackNavigator { // swiftlint:d showSettings(presentationMode: presentationMode) case let .shareURL(url): showShareSheet([url]) - case let .shareURLs(urls): - showShareSheet(urls) case .vault: showVault() case .vaultUnlockSetup: @@ -343,17 +340,6 @@ final class SettingsCoordinator: Coordinator, HasStackNavigator { // swiftlint:d stackNavigator?.present(DeleteAccountView(store: Store(processor: processor))) } - /// Shows the enable flight recorder screen. - /// - private func showEnableFlightRecorder() { - let processor = EnableFlightRecorderProcessor( - coordinator: asAnyCoordinator(), - services: services, - state: EnableFlightRecorderState(), - ) - stackNavigator?.present(EnableFlightRecorderView(store: Store(processor: processor))) - } - /// Shows the share sheet to share one or more items. /// /// - Parameter items: The items to share. @@ -400,16 +386,15 @@ final class SettingsCoordinator: Coordinator, HasStackNavigator { // swiftlint:d stackNavigator?.present(navigationController) } - /// Shows the flight recorder logs screen. + /// Shows a flight recorder view. /// - private func showFlightRecorderLogs() { - let processor = FlightRecorderLogsProcessor( - coordinator: asAnyCoordinator(), - services: services, - state: FlightRecorderLogsState(), - ) - let view = FlightRecorderLogsView(store: Store(processor: processor), timeProvider: services.timeProvider) - stackNavigator?.present(view) + /// - Parameter route: A `FlightRecorderRoute` to navigate to. + /// + private func showFlightRecorder(route: FlightRecorderRoute) { + guard let stackNavigator else { return } + let coordinator = module.makeFlightRecorderCoordinator(stackNavigator: stackNavigator) + coordinator.start() + coordinator.navigate(to: route) } /// Shows the folders screen. diff --git a/BitwardenShared/UI/Platform/Settings/SettingsCoordinatorTests.swift b/BitwardenShared/UI/Platform/Settings/SettingsCoordinatorTests.swift index 6d3e225f2..576fb9722 100644 --- a/BitwardenShared/UI/Platform/Settings/SettingsCoordinatorTests.swift +++ b/BitwardenShared/UI/Platform/Settings/SettingsCoordinatorTests.swift @@ -175,17 +175,6 @@ class SettingsCoordinatorTests: BitwardenTestCase { // swiftlint:disable:this ty XCTAssertEqual(action.type, .dismissed) } - /// `navigate(to:)` with `.enableFlightRecorder` presents the enable flight recorder view. - @MainActor - func test_navigateTo_enableFlightRecorder() throws { - subject.navigate(to: .enableFlightRecorder) - - let action = try XCTUnwrap(stackNavigator.actions.last) - XCTAssertEqual(action.type, .presented) - XCTAssertTrue(action.view is EnableFlightRecorderView) - XCTAssertEqual(action.embedInNavigationController, true) - } - /// `navigate(to:)` with `.exportVault` presents the export vault to file view when /// Credential Exchange flag to export is disabled. @MainActor @@ -262,15 +251,14 @@ class SettingsCoordinatorTests: BitwardenTestCase { // swiftlint:disable:this ty XCTAssertEqual(module.importLoginsCoordinator.routes.last, .importLogins(.settings)) } - /// `navigate(to:)` with `.flightRecorderLogs` presents the flight recorder logs view. + /// `navigate(to:)` with `.flightRecorder` starts flight recorder coordinator and navigates to + /// the enable flight recorder view. @MainActor - func test_navigateTo_flightRecorderLogs() throws { - subject.navigate(to: .flightRecorderLogs) + func test_navigateTo_flightRecorder() throws { + subject.navigate(to: .flightRecorder(.enableFlightRecorder)) - let action = try XCTUnwrap(stackNavigator.actions.last) - XCTAssertEqual(action.type, .presented) - XCTAssertTrue(action.view is FlightRecorderLogsView) - XCTAssertEqual(action.embedInNavigationController, true) + XCTAssertTrue(module.flightRecorderCoordinator.isStarted) + XCTAssertEqual(module.flightRecorderCoordinator.routes.last, .enableFlightRecorder) } /// `navigate(to:)` with `.lockVault` navigates the user to the login view. @@ -404,16 +392,6 @@ class SettingsCoordinatorTests: BitwardenTestCase { // swiftlint:disable:this ty XCTAssertTrue(action.view is UIActivityViewController) } - /// `navigate(to:)` with `.shareURL(_:)` presents an activity view controller to share the URLs. - @MainActor - func test_navigateTo_shareURLs() throws { - subject.navigate(to: .shareURLs([.example])) - - let action = try XCTUnwrap(stackNavigator.actions.last) - XCTAssertEqual(action.type, .presented) - XCTAssertTrue(action.view is UIActivityViewController) - } - /// `navigate(to:)` with `.vault` pushes the vault settings view onto the stack navigator. @MainActor func test_navigateTo_vault() throws { diff --git a/BitwardenShared/UI/Platform/Settings/SettingsRoute.swift b/BitwardenShared/UI/Platform/Settings/SettingsRoute.swift index 9c9c070fb..260ad3f3a 100644 --- a/BitwardenShared/UI/Platform/Settings/SettingsRoute.swift +++ b/BitwardenShared/UI/Platform/Settings/SettingsRoute.swift @@ -35,9 +35,6 @@ public enum SettingsRoute: Equatable, Hashable { /// A route that dismisses the current view. case dismiss - /// A route to enable and configure flight recorder. - case enableFlightRecorder - /// A route to the export vault settings view or export to file view depending on feature flag. case exportVault @@ -47,8 +44,8 @@ public enum SettingsRoute: Equatable, Hashable { /// A route to the export vault to file view. case exportVaultToFile - /// A route to the flight recorder logs screen. - case flightRecorderLogs + /// A route to a flight recorder view. + case flightRecorder(FlightRecorderRoute) /// A route to view the folders in the vault. case folders @@ -83,9 +80,6 @@ public enum SettingsRoute: Equatable, Hashable { /// A route to the share sheet to share a URL. case shareURL(URL) - /// A route to the share sheet to share multiple URLs. - case shareURLs([URL]) - /// A route to the vault settings view. case vault diff --git a/GlobalTestHelpers/MockAppModule.swift b/GlobalTestHelpers/MockAppModule.swift index b0348e0e6..43b54b528 100644 --- a/GlobalTestHelpers/MockAppModule.swift +++ b/GlobalTestHelpers/MockAppModule.swift @@ -14,6 +14,7 @@ class MockAppModule: ExportCXFModule, ExtensionSetupModule, FileSelectionModule, + FlightRecorderModule, GeneratorModule, ImportCXFModule, ImportLoginsModule, @@ -38,6 +39,7 @@ class MockAppModule: var extensionSetupCoordinator = MockCoordinator() var fileSelectionDelegate: FileSelectionDelegate? var fileSelectionCoordinator = MockCoordinator() + var flightRecorderCoordinator = MockCoordinator() var generatorCoordinator = MockCoordinator() var importCXFCoordinator = MockCoordinator() var importLoginsCoordinator = MockCoordinator() @@ -109,6 +111,12 @@ class MockAppModule: return fileSelectionCoordinator.asAnyCoordinator() } + func makeFlightRecorderCoordinator( + stackNavigator _: StackNavigator, + ) -> AnyCoordinator { + flightRecorderCoordinator.asAnyCoordinator() + } + func makeGeneratorCoordinator( delegate _: GeneratorCoordinatorDelegate?, stackNavigator _: StackNavigator, diff --git a/swiftgen-bwr.yml b/swiftgen-bwr.yml index 6b6ab0e8e..873b654ec 100644 --- a/swiftgen-bwr.yml +++ b/swiftgen-bwr.yml @@ -20,6 +20,7 @@ xcassets: inputs: - BitwardenResources/Colors.xcassets - BitwardenResources/Icons.xcassets + - BitwardenResources/Illustrations.xcassets outputs: - templateName: swift5 output: SharedAssets.swift