diff --git a/AuthenticatorShared/UI/DebugMenu/DebugMenuAction.swift b/AuthenticatorShared/UI/DebugMenu/DebugMenuAction.swift deleted file mode 100644 index fdb2c089d7..0000000000 --- a/AuthenticatorShared/UI/DebugMenu/DebugMenuAction.swift +++ /dev/null @@ -1,10 +0,0 @@ -import Foundation - -// MARK: - DebugMenuAction - -/// Actions that can be processed by a `DebugMenuProcessor`. -/// -enum DebugMenuAction: Equatable { - /// The dismiss button was tapped. - case dismissTapped -} diff --git a/AuthenticatorShared/UI/DebugMenu/DebugMenuCoordinator.swift b/AuthenticatorShared/UI/DebugMenu/DebugMenuCoordinator.swift deleted file mode 100644 index c8665944ad..0000000000 --- a/AuthenticatorShared/UI/DebugMenu/DebugMenuCoordinator.swift +++ /dev/null @@ -1,75 +0,0 @@ -import BitwardenKit -import Foundation - -/// A coordinator that manages navigation for the debug menu. -/// -final class DebugMenuCoordinator: Coordinator, HasStackNavigator { - // MARK: Types - - typealias Services = HasAppSettingsStore - & HasConfigService - & HasErrorAlertServices.ErrorAlertServices - - // MARK: Private Properties - - /// The services used by this coordinator. - private let services: Services - - // MARK: Properties - - /// The stack navigator that is managed by this coordinator. - private(set) weak var stackNavigator: StackNavigator? - - // MARK: Initialization - - /// Creates a new `DebugMenuCoordinator`. - /// - /// - Parameters: - /// - services: The services used by this coordinator. - /// - stackNavigator: The stack navigator that is managed by this coordinator. - /// - init( - services: Services, - stackNavigator: StackNavigator, - ) { - self.services = services - self.stackNavigator = stackNavigator - } - - // MARK: Methods - - func navigate( - to route: DebugMenuRoute, - context: AnyObject?, - ) { - switch route { - case .dismiss: - stackNavigator?.dismiss() - } - } - - /// Starts the process of displaying the debug menu. - func start() { - showDebugMenu() - } - - // MARK: Private Methods - - /// Configures and displays the debug menu. - private func showDebugMenu() { - let processor = DebugMenuProcessor( - coordinator: asAnyCoordinator(), - services: services, - state: DebugMenuState(), - ) - - let view = DebugMenuView(store: Store(processor: processor)) - stackNavigator?.replace(view) - } -} - -// MARK: - HasErrorAlertServices - -extension DebugMenuCoordinator: HasErrorAlertServices { - var errorAlertServices: ErrorAlertServices { services } -} diff --git a/AuthenticatorShared/UI/DebugMenu/DebugMenuCoordinatorTests.swift b/AuthenticatorShared/UI/DebugMenu/DebugMenuCoordinatorTests.swift deleted file mode 100644 index f9e5ede7ef..0000000000 --- a/AuthenticatorShared/UI/DebugMenu/DebugMenuCoordinatorTests.swift +++ /dev/null @@ -1,60 +0,0 @@ -import BitwardenKitMocks -import SwiftUI -import XCTest - -@testable import AuthenticatorShared - -class DebugMenuCoordinatorTests: BitwardenTestCase { - // MARK: Properties - - var appSettingsStore: MockAppSettingsStore! - var configService: MockConfigService! - var stackNavigator: MockStackNavigator! - var subject: DebugMenuCoordinator! - - // MARK: Setup & Teardown - - override func setUp() { - super.setUp() - - appSettingsStore = MockAppSettingsStore() - configService = MockConfigService() - stackNavigator = MockStackNavigator() - - subject = DebugMenuCoordinator( - services: ServiceContainer.withMocks( - appSettingsStore: appSettingsStore, - configService: configService, - ), - stackNavigator: stackNavigator, - ) - } - - override func tearDown() { - super.tearDown() - - appSettingsStore = nil - configService = nil - stackNavigator = nil - subject = nil - } - - // MARK: Tests - - /// `navigate(to:)` with `.dismiss` dismisses the view. - @MainActor - func test_navigate_dismiss() throws { - subject.navigate(to: .dismiss) - - let action = try XCTUnwrap(stackNavigator.actions.last) - XCTAssertEqual(action.type, .dismissed) - } - - /// `start()` correctly shows the `DebugMenuView`. - @MainActor - func test_start() { - subject.start() - - XCTAssertTrue(stackNavigator.actions.last?.view is DebugMenuView) - } -} diff --git a/AuthenticatorShared/UI/DebugMenu/DebugMenuEffect.swift b/AuthenticatorShared/UI/DebugMenu/DebugMenuEffect.swift deleted file mode 100644 index e76999ed8d..0000000000 --- a/AuthenticatorShared/UI/DebugMenu/DebugMenuEffect.swift +++ /dev/null @@ -1,20 +0,0 @@ -import Foundation - -// MARK: - DebugMenuEffect - -/// Effects that can be processed by a `DebugMenuProcessor`. -/// -enum DebugMenuEffect: Equatable { - /// Triggers a refresh of feature flags, clearing local settings and re-fetching from the remote source. - case refreshFeatureFlags - - /// Toggles a specific feature flag's state. - /// - /// - Parameters: - /// - String: The identifier for the feature flag. - /// - Bool: The state to which the feature flag should be set (enabled or disabled). - case toggleFeatureFlag(String, Bool) - - /// The view appeared and is ready to load data. - case viewAppeared -} diff --git a/AuthenticatorShared/UI/DebugMenu/DebugMenuProcessor.swift b/AuthenticatorShared/UI/DebugMenu/DebugMenuProcessor.swift deleted file mode 100644 index 937d42cf10..0000000000 --- a/AuthenticatorShared/UI/DebugMenu/DebugMenuProcessor.swift +++ /dev/null @@ -1,75 +0,0 @@ -import BitwardenKit -import Foundation - -// MARK: - DebugMenuProcessor - -/// The processor used to manage state and handle actions for the `DebugMenuView`. -/// -final class DebugMenuProcessor: StateProcessor { - // MARK: Types - - typealias Services = HasConfigService - - // MARK: Properties - - /// The `Coordinator` that handles navigation. - private let coordinator: AnyCoordinator - - /// The services used by the processor. - private let services: Services - - // MARK: Initialization - - /// Initializes a `DebugMenuProcessor`. - /// - /// - Parameters: - /// - coordinator: The coordinator used for navigation. - /// - services: The services used by the processor. - /// - state: The state of the debug menu. - /// - init( - coordinator: AnyCoordinator, - services: Services, - state: DebugMenuState, - ) { - self.coordinator = coordinator - self.services = services - super.init(state: state) - } - - // MARK: Methods - - override func receive(_ action: DebugMenuAction) { - switch action { - case .dismissTapped: - coordinator.navigate(to: .dismiss) - } - } - - override func perform(_ effect: DebugMenuEffect) async { - switch effect { - case .viewAppeared: - await fetchFlags() - case .refreshFeatureFlags: - await refreshFlags() - case let .toggleFeatureFlag(flag, newValue): - await services.configService.toggleDebugFeatureFlag( - name: flag, - newValue: newValue, - ) - state.featureFlags = await services.configService.getDebugFeatureFlags(FeatureFlag.allCases) - } - } - - // MARK: Private Functions - - /// Fetch the current debug feature flags. - private func fetchFlags() async { - state.featureFlags = await services.configService.getDebugFeatureFlags(FeatureFlag.allCases) - } - - /// Refreshes the feature flags by resetting their local values and fetching the latest configurations. - private func refreshFlags() async { - state.featureFlags = await services.configService.refreshDebugFeatureFlags(FeatureFlag.allCases) - } -} diff --git a/AuthenticatorShared/UI/DebugMenu/DebugMenuProcessorTests.swift b/AuthenticatorShared/UI/DebugMenu/DebugMenuProcessorTests.swift deleted file mode 100644 index 57c0cd9021..0000000000 --- a/AuthenticatorShared/UI/DebugMenu/DebugMenuProcessorTests.swift +++ /dev/null @@ -1,88 +0,0 @@ -import BitwardenKit -import BitwardenKitMocks -import XCTest - -@testable import AuthenticatorShared - -class DebugMenuProcessorTests: BitwardenTestCase { - // MARK: Properties - - var configService: MockConfigService! - var coordinator: MockCoordinator! - var subject: DebugMenuProcessor! - - // MARK: Set Up & Tear Down - - override func setUp() { - super.setUp() - - configService = MockConfigService() - coordinator = MockCoordinator() - subject = DebugMenuProcessor( - coordinator: coordinator.asAnyCoordinator(), - services: ServiceContainer.withMocks( - configService: configService, - ), - state: DebugMenuState(featureFlags: []), - ) - } - - override func tearDown() { - super.tearDown() - - configService = nil - coordinator = nil - subject = nil - } - - // MARK: Tests - - /// `receive()` with `.dismissTapped` navigates to the `.dismiss` route. - @MainActor - func test_receive_dismissTapped() { - subject.receive(.dismissTapped) - XCTAssertEqual(coordinator.routes.last, .dismiss) - } - - /// `perform(.viewAppeared)` loads the correct feature flags. - @MainActor - func test_perform_appeared_loadsFeatureFlags() async { - XCTAssertTrue(subject.state.featureFlags.isEmpty) - - let flag = DebugMenuFeatureFlag( - feature: .testFeatureFlag, - isEnabled: false, - ) - - configService.debugFeatureFlags = [flag] - - await subject.perform(.viewAppeared) - - XCTAssertTrue(subject.state.featureFlags.contains(flag)) - } - - /// `perform(.refreshFeatureFlags)` refreshs the current feature flags. - @MainActor - func test_perform_refreshFeatureFlags() async { - await subject.perform(.refreshFeatureFlags) - XCTAssertTrue(configService.refreshDebugFeatureFlagsCalled) - } - - /// `perform(.toggleFeatureFlag)` changes the state of the feature flag. - @MainActor - func test_perform_toggleFeatureFlag() async { - let flag = DebugMenuFeatureFlag( - feature: .testFeatureFlag, - isEnabled: true, - ) - - await subject.perform( - .toggleFeatureFlag( - flag.feature.rawValue, - false, - ), - ) - - XCTAssertTrue(configService.toggleDebugFeatureFlagCalled) - } -} diff --git a/AuthenticatorShared/UI/DebugMenu/DebugMenuState.swift b/AuthenticatorShared/UI/DebugMenu/DebugMenuState.swift deleted file mode 100644 index be822478c0..0000000000 --- a/AuthenticatorShared/UI/DebugMenu/DebugMenuState.swift +++ /dev/null @@ -1,11 +0,0 @@ -import BitwardenKit -import Foundation - -// MARK: - DebugMenuState - -/// The state used to present the `DebugMenuView`. -/// -struct DebugMenuState: Equatable, Sendable { - /// The current feature flags supported. - var featureFlags: [DebugMenuFeatureFlag] = [] -} diff --git a/AuthenticatorShared/UI/DebugMenu/DebugMenuView+SnapshotTests.swift b/AuthenticatorShared/UI/DebugMenu/DebugMenuView+SnapshotTests.swift deleted file mode 100644 index 642f3322ce..0000000000 --- a/AuthenticatorShared/UI/DebugMenu/DebugMenuView+SnapshotTests.swift +++ /dev/null @@ -1,58 +0,0 @@ -// swiftlint:disable:this file_name -import BitwardenKit -import BitwardenKitMocks -import BitwardenResources -import SnapshotTesting -import XCTest - -@testable import AuthenticatorShared - -// MARK: - DebugMenuViewTests - -class DebugMenuViewTests: BitwardenTestCase { - // MARK: Properties - - var processor: MockProcessor! - var subject: DebugMenuView! - - // MARK: Setup & Teardown - - override func setUp() { - super.setUp() - - processor = MockProcessor( - state: DebugMenuState( - featureFlags: [ - .init( - feature: .testFeatureFlag, - isEnabled: false, - ), - ], - ), - ) - let store = Store(processor: processor) - - subject = DebugMenuView(store: store) - } - - override func tearDown() { - super.tearDown() - - processor = nil - subject = nil - } - - // MARK: Tests - - /// Check the snapshot when feature flags are enabled and disabled. - @MainActor - func disabletest_snapshot_debugMenuWithFeatureFlags() { - processor.state.featureFlags = [ - .init( - feature: .testFeatureFlag, - isEnabled: true, - ), - ] - assertSnapshot(of: subject, as: .defaultPortrait) - } -} diff --git a/AuthenticatorShared/UI/DebugMenu/DebugMenuView+ViewInspectorTests.swift b/AuthenticatorShared/UI/DebugMenu/DebugMenuView+ViewInspectorTests.swift deleted file mode 100644 index e6a658d4b2..0000000000 --- a/AuthenticatorShared/UI/DebugMenu/DebugMenuView+ViewInspectorTests.swift +++ /dev/null @@ -1,75 +0,0 @@ -// swiftlint:disable:this file_name -import BitwardenKit -import BitwardenKitMocks -import BitwardenResources -import ViewInspector -import ViewInspectorTestHelpers -import XCTest - -@testable import AuthenticatorShared - -// MARK: - DebugMenuViewTests - -class DebugMenuViewTests: BitwardenTestCase { - // MARK: Properties - - var processor: MockProcessor! - var subject: DebugMenuView! - - // MARK: Setup & Teardown - - override func setUp() { - super.setUp() - - processor = MockProcessor( - state: DebugMenuState( - featureFlags: [ - .init( - feature: .testFeatureFlag, - isEnabled: false, - ), - ], - ), - ) - let store = Store(processor: processor) - - subject = DebugMenuView(store: store) - } - - override func tearDown() { - super.tearDown() - - processor = nil - subject = nil - } - - // MARK: Tests - - /// Tapping the close button dispatches the `.dismissTapped` action. - @MainActor - func test_closeButton_tap() throws { - let button = try subject.inspect().find(button: Localizations.close) - try button.tap() - XCTAssertEqual(processor.dispatchedActions.last, .dismissTapped) - } - - /// Tests that the toggle fires off the correct effect. - @MainActor - func test_featureFlag_toggled() async throws { - if #available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, *) { - throw XCTSkip("Unable to run test in iOS 16, keep an eye on ViewInspector to see if it gets updated.") - } - let featureFlagName = FeatureFlag.testFeatureFlag.rawValue - let toggle = try subject.inspect().find(viewWithAccessibilityIdentifier: featureFlagName).toggle() - try toggle.tap() - XCTAssertEqual(processor.effects.last, .toggleFeatureFlag(featureFlagName, true)) - } - - /// Test that the refresh button sends the correct effect. - @MainActor - func disabletest_refreshFeatureFlags_tapped() async throws { - let button = try subject.inspect().find(asyncButtonWithAccessibilityLabel: "RefreshFeatureFlagsButton") - try await button.tap() - XCTAssertEqual(processor.effects.last, .refreshFeatureFlags) - } -} diff --git a/AuthenticatorShared/UI/DebugMenu/DebugMenuView.swift b/AuthenticatorShared/UI/DebugMenu/DebugMenuView.swift deleted file mode 100644 index 9d180ed003..0000000000 --- a/AuthenticatorShared/UI/DebugMenu/DebugMenuView.swift +++ /dev/null @@ -1,85 +0,0 @@ -import BitwardenKit -import BitwardenResources -import SwiftUI - -// MARK: - DebugMenuView - -/// Represents the debug menu for configuring app settings and feature flags. -/// -struct DebugMenuView: View { - // MARK: Properties - - /// The store used to render the view. - @ObservedObject var store: Store - - // MARK: View - - var body: some View { - List { - Section { - featureFlags - } header: { - featureFlagSectionHeader - } - } - .toolbar { - ToolbarItem(placement: .navigationBarTrailing) { - Button { - store.send(.dismissTapped) - } label: { - Text(Localizations.close) - } - .accessibilityIdentifier("close-debug") - } - } - .navigationTitle("Debug Menu") - .task { - await store.perform(.viewAppeared) - } - } - - /// The feature flags currently used in the app. - private var featureFlags: some View { - ForEach(store.state.featureFlags) { flag in - Toggle( - isOn: store.bindingAsync( - get: { _ in flag.isEnabled }, - perform: { DebugMenuEffect.toggleFeatureFlag(flag.feature.rawValue, $0) }, - ), - ) { - Text(flag.feature.name) - } - .toggleStyle(.bitwarden) - .accessibilityIdentifier(flag.feature.rawValue) - } - } - - /// The header for the feature flags section. - private var featureFlagSectionHeader: some View { - HStack { - Text("Feature Flags") - Spacer() - AsyncButton { - await store.perform(.refreshFeatureFlags) - } label: { - Image(systemName: "arrow.clockwise") - } - .accessibilityLabel("RefreshFeatureFlagsButton") - } - } -} - -#if DEBUG -#Preview { - DebugMenuView( - store: Store( - processor: StateProcessor( - state: .init( - featureFlags: [ - ], - ), - ), - ), - ) -} -#endif diff --git a/AuthenticatorShared/UI/DebugMenu/ShakeWindow.swift b/AuthenticatorShared/UI/DebugMenu/ShakeWindow.swift deleted file mode 100644 index fbbec964a7..0000000000 --- a/AuthenticatorShared/UI/DebugMenu/ShakeWindow.swift +++ /dev/null @@ -1,55 +0,0 @@ -import UIKit - -/// A UIWindow subclass that detects and responds to shake gestures. -/// -/// This window class allows you to provide a custom handler that will be called whenever a shake -/// gesture is detected. This can be particularly useful for triggering debug or testing actions only -/// in DEBUG_MENU mode, such as showing development menus or refreshing data. -/// -public class ShakeWindow: UIWindow { - /// The callback to be invoked when a shake gesture is detected. - public var onShakeDetected: (() -> Void)? - - /// Initializes a new ShakeWindow with a specific window scene and an optional shake detection handler. - /// - /// - Parameters: - /// - windowScene: The UIWindowScene instance with which the window is associated. - /// - onShakeDetected: An optional closure that gets called when a shake gesture is detected. - /// - public init( - windowScene: UIWindowScene, - onShakeDetected: (() -> Void)?, - ) { - self.onShakeDetected = onShakeDetected - super.init(windowScene: windowScene) - } - - /// Required initializer for UIWindow subclass. Not implemented as ShakeWindow requires - /// a custom initialization method with shake detection handler. - /// - /// - Parameter coder: An NSCoder instance for decoding the window. - /// - @available(*, unavailable) - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - /// Overrides the default motionEnded function to detect shake motions. - /// If a shake motion is detected and we are in DEBUG_MENU mode, - /// the onShakeDetected closure is called. - /// - /// - Parameters: - /// - motion: An event-subtype constant indicating the kind of motion. - /// - event: An object representing the event associated with the motion. - /// - override public func motionEnded( - _ motion: UIEvent.EventSubtype, - with event: UIEvent?, - ) { - #if DEBUG_MENU - if motion == .motionShake { - onShakeDetected?() - } - #endif - } -} diff --git a/AuthenticatorShared/UI/DebugMenu/__Snapshots__/DebugMenuViewTests/test_snapshot_debugMenuWithFeatureFlags.1.png b/AuthenticatorShared/UI/DebugMenu/__Snapshots__/DebugMenuViewTests/test_snapshot_debugMenuWithFeatureFlags.1.png deleted file mode 100644 index e28cfcf40b..0000000000 Binary files a/AuthenticatorShared/UI/DebugMenu/__Snapshots__/DebugMenuViewTests/test_snapshot_debugMenuWithFeatureFlags.1.png and /dev/null differ diff --git a/AuthenticatorShared/UI/Platform/Application/AppCoordinator.swift b/AuthenticatorShared/UI/Platform/Application/AppCoordinator.swift index fa6a3a2a14..da03fbbbf6 100644 --- a/AuthenticatorShared/UI/Platform/Application/AppCoordinator.swift +++ b/AuthenticatorShared/UI/Platform/Application/AppCoordinator.swift @@ -26,6 +26,9 @@ class AppCoordinator: Coordinator, HasRootNavigator { /// The coordinator currently being displayed. private var childCoordinator: AnyObject? + /// Whether the debug menu is currently being shown. + private(set) var isShowingDebugMenu = false + // MARK: Properties /// The module to use for creating child coordinators. @@ -84,9 +87,7 @@ class AppCoordinator: Coordinator, HasRootNavigator { func navigate(to route: AppRoute, context _: AnyObject?) { switch route { case .debugMenu: - #if DEBUG_MENU showDebugMenu() - #endif case let .tab(tabRoute): showTab(route: tabRoute) } @@ -160,7 +161,6 @@ class AppCoordinator: Coordinator, HasRootNavigator { rootNavigator?.rootViewController?.present(navigationController, animated: false) } - #if DEBUG_MENU /// Configures and presents the debug menu. /// /// Initializes feedback generator for haptic feedback. Sets up a `UINavigationController` @@ -168,22 +168,23 @@ class AppCoordinator: Coordinator, HasRootNavigator { /// Presents the navigation controller and triggers haptic feedback upon completion. /// private func showDebugMenu() { + guard !isShowingDebugMenu else { return } + let feedbackGenerator = UIImpactFeedbackGenerator(style: .medium) feedbackGenerator.prepare() let stackNavigator = UINavigationController() stackNavigator.navigationBar.prefersLargeTitles = true stackNavigator.modalPresentationStyle = .fullScreen - let debugMenuCoordinator = module.makeDebugMenuCoordinator(stackNavigator: stackNavigator) + let debugMenuCoordinator = module.makeDebugMenuCoordinator(delegate: self, stackNavigator: stackNavigator) debugMenuCoordinator.start() - childCoordinator = debugMenuCoordinator rootNavigator?.rootViewController?.topmostViewController().present( stackNavigator, animated: true, completion: { feedbackGenerator.impactOccurred() }, ) + isShowingDebugMenu = true } - #endif } // MARK: - AuthCoordinatorDelegate @@ -194,6 +195,14 @@ extension AppCoordinator: AuthCoordinatorDelegate { } } +// MARK: - DebugMenuCoordinatorDelegate + +extension AppCoordinator: DebugMenuCoordinatorDelegate { + func didDismissDebugMenu() { + isShowingDebugMenu = false + } +} + // MARK: - HasErrorAlertServices extension AppCoordinator: HasErrorAlertServices { diff --git a/AuthenticatorShared/UI/Platform/Application/AppModule.swift b/AuthenticatorShared/UI/Platform/Application/AppModule.swift index 001046ada3..dc996edf62 100644 --- a/AuthenticatorShared/UI/Platform/Application/AppModule.swift +++ b/AuthenticatorShared/UI/Platform/Application/AppModule.swift @@ -58,6 +58,22 @@ extension DefaultAppModule: AppModule { } } +// MARK: - DefaultAppModule + DebugMenuModule + +extension DefaultAppModule: DebugMenuModule { + public func makeDebugMenuCoordinator( + delegate: DebugMenuCoordinatorDelegate, + stackNavigator: StackNavigator, + ) -> AnyCoordinator { + DebugMenuCoordinator( + delegate: delegate, + services: services, + stackNavigator: stackNavigator, + ) + .asAnyCoordinator() + } +} + // MARK: - DefaultAppModule + FlightRecorderModule extension DefaultAppModule: FlightRecorderModule { diff --git a/AuthenticatorShared/UI/Platform/Application/AppModuleTests.swift b/AuthenticatorShared/UI/Platform/Application/AppModuleTests.swift index d77287846f..3564e69db5 100644 --- a/AuthenticatorShared/UI/Platform/Application/AppModuleTests.swift +++ b/AuthenticatorShared/UI/Platform/Application/AppModuleTests.swift @@ -38,6 +38,19 @@ class AppModuleTests: BitwardenTestCase { XCTAssertNotNil(rootViewController.childViewController) } + /// `makeDebugMenuCoordinator()` builds the debug menu coordinator. + @MainActor + func test_makeDebugMenuCoordinator() { + let navigationController = UINavigationController() + let coordinator = subject.makeDebugMenuCoordinator( + delegate: MockDebugMenuCoordinatorDelegate(), + stackNavigator: navigationController, + ) + coordinator.start() + XCTAssertEqual(navigationController.viewControllers.count, 1) + XCTAssertTrue(navigationController.viewControllers[0] is UIHostingController) + } + /// `makeNavigationController()` builds a navigation controller. @MainActor func test_makeNavigationController() { diff --git a/BitwardenKit/Core/Platform/Extensions/Array+Extensions.swift b/BitwardenKit/Core/Platform/Extensions/Array+Extensions.swift new file mode 100644 index 0000000000..aa2334f8cc --- /dev/null +++ b/BitwardenKit/Core/Platform/Extensions/Array+Extensions.swift @@ -0,0 +1,22 @@ +// MARK: - Array + Extensions + +public extension Array { + /// Safely access elements in an array by index without running into an out-of-bounds error. + /// This works like normal array subscript access, but if the index is out of bounds, then + /// returns nil instead of throwing an error. This can be useful in cases, particularly in tests, + /// where we want to access array elements by index number, and not have additional error handling + /// if the index in question does not exist in the array. + /// + /// This can be used in a subscript. For example, `array[safeIndex: 2]`. + /// + /// - Parameters: + /// - safeIndex: The position of the element to access. + /// - Returns: The element at the specified index if it is within bounds, otherwise `nil`. + subscript(safeIndex index: Int) -> Element? { + guard index >= 0, index < endIndex else { + return nil + } + + return self[index] + } +} diff --git a/BitwardenShared/Core/Platform/Extensions/ArrayExtensionsTests.swift b/BitwardenKit/Core/Platform/Extensions/ArrayExtensionsTests.swift similarity index 96% rename from BitwardenShared/Core/Platform/Extensions/ArrayExtensionsTests.swift rename to BitwardenKit/Core/Platform/Extensions/ArrayExtensionsTests.swift index 2689239f82..a426fee917 100644 --- a/BitwardenShared/Core/Platform/Extensions/ArrayExtensionsTests.swift +++ b/BitwardenKit/Core/Platform/Extensions/ArrayExtensionsTests.swift @@ -1,7 +1,6 @@ +import BitwardenKit import XCTest -@testable import BitwardenShared - class ArrayExtensionsTests: BitwardenTestCase { // MARK: Tests diff --git a/BitwardenKit/Core/Platform/Utilities/ErrorReportBuilderTests.swift b/BitwardenKit/Core/Platform/Utilities/ErrorReportBuilderTests.swift index 03601d7d13..66e35a5851 100644 --- a/BitwardenKit/Core/Platform/Utilities/ErrorReportBuilderTests.swift +++ b/BitwardenKit/Core/Platform/Utilities/ErrorReportBuilderTests.swift @@ -78,6 +78,7 @@ class ErrorReportBuilderTests: BitwardenTestCase { AuthenticatorBridgeKitMocks: 0x0000000000000000 BitwardenKit: 0x0000000000000000 BitwardenKitMocks: 0x0000000000000000 + BitwardenSdk_464161978EAC5FCE_PackageProduct: 0x0000000000000000 BitwardenResources: 0x0000000000000000 AuthenticatorBridgeKit: 0x0000000000000000 @@ -115,6 +116,7 @@ class ErrorReportBuilderTests: BitwardenTestCase { AuthenticatorBridgeKitMocks: 0x0000000000000000 BitwardenKit: 0x0000000000000000 BitwardenKitMocks: 0x0000000000000000 + BitwardenSdk_464161978EAC5FCE_PackageProduct: 0x0000000000000000 BitwardenResources: 0x0000000000000000 AuthenticatorBridgeKit: 0x0000000000000000 @@ -150,6 +152,7 @@ class ErrorReportBuilderTests: BitwardenTestCase { AuthenticatorBridgeKitMocks: 0x0000000000000000 BitwardenKit: 0x0000000000000000 BitwardenKitMocks: 0x0000000000000000 + BitwardenSdk_464161978EAC5FCE_PackageProduct: 0x0000000000000000 BitwardenResources: 0x0000000000000000 AuthenticatorBridgeKit: 0x0000000000000000 diff --git a/BitwardenShared/UI/Platform/DebugMenu/ShakeWindow.swift b/BitwardenKit/UI/Platform/Application/Views/ShakeWindow.swift similarity index 83% rename from BitwardenShared/UI/Platform/DebugMenu/ShakeWindow.swift rename to BitwardenKit/UI/Platform/Application/Views/ShakeWindow.swift index fbbec964a7..69876391a3 100644 --- a/BitwardenShared/UI/Platform/DebugMenu/ShakeWindow.swift +++ b/BitwardenKit/UI/Platform/Application/Views/ShakeWindow.swift @@ -3,8 +3,11 @@ import UIKit /// A UIWindow subclass that detects and responds to shake gestures. /// /// This window class allows you to provide a custom handler that will be called whenever a shake -/// gesture is detected. This can be particularly useful for triggering debug or testing actions only -/// in DEBUG_MENU mode, such as showing development menus or refreshing data. +/// gesture is detected. **Note:** The shake detection only functions when compiled with the +/// `DEBUG_MENU` conditional compilation flag. In release builds, shake gestures are ignored. +/// +/// This is particularly useful for triggering debug menus or testing actions in development builds +/// while ensuring the functionality is completely removed from production builds. /// public class ShakeWindow: UIWindow { /// The callback to be invoked when a shake gesture is detected. diff --git a/BitwardenShared/UI/Platform/DebugMenu/DebugMenuAction.swift b/BitwardenKit/UI/Platform/DebugMenu/DebugMenuAction.swift similarity index 79% rename from BitwardenShared/UI/Platform/DebugMenu/DebugMenuAction.swift rename to BitwardenKit/UI/Platform/DebugMenu/DebugMenuAction.swift index bc7f904424..d01f049743 100644 --- a/BitwardenShared/UI/Platform/DebugMenu/DebugMenuAction.swift +++ b/BitwardenKit/UI/Platform/DebugMenu/DebugMenuAction.swift @@ -1,5 +1,3 @@ -import Foundation - // MARK: - DebugMenuAction /// Actions that can be processed by a `DebugMenuProcessor`. @@ -11,4 +9,6 @@ enum DebugMenuAction: Equatable { case generateCrash /// The generate error report button was tapped. case generateErrorReport + /// The generate SDK error report button was tapped. + case generateSdkErrorReport } diff --git a/BitwardenShared/UI/Platform/DebugMenu/DebugMenuCoordinator.swift b/BitwardenKit/UI/Platform/DebugMenu/DebugMenuCoordinator.swift similarity index 84% rename from BitwardenShared/UI/Platform/DebugMenu/DebugMenuCoordinator.swift rename to BitwardenKit/UI/Platform/DebugMenu/DebugMenuCoordinator.swift index b9778e46c1..bd80f9c366 100644 --- a/BitwardenShared/UI/Platform/DebugMenu/DebugMenuCoordinator.swift +++ b/BitwardenKit/UI/Platform/DebugMenu/DebugMenuCoordinator.swift @@ -1,9 +1,8 @@ -import BitwardenKit import Foundation /// An object that is notified when the debug menu is dismissed. /// -protocol DebugMenuCoordinatorDelegate: AnyObject { +public protocol DebugMenuCoordinatorDelegate: AnyObject { // sourcery: AutoMockable /// The debug menu has been dismissed. /// func didDismissDebugMenu() @@ -11,11 +10,10 @@ protocol DebugMenuCoordinatorDelegate: AnyObject { /// A coordinator that manages navigation for the debug menu. /// -final class DebugMenuCoordinator: Coordinator, HasStackNavigator { +public final class DebugMenuCoordinator: Coordinator, HasStackNavigator { // MARK: Types - typealias Services = HasAppSettingsStore - & HasConfigService + public typealias Services = HasConfigService & HasErrorAlertServices.ErrorAlertServices & HasErrorReporter @@ -30,7 +28,7 @@ final class DebugMenuCoordinator: Coordinator, HasStackNavigator { // MARK: Properties /// The stack navigator that is managed by this coordinator. - private(set) weak var stackNavigator: StackNavigator? + public private(set) weak var stackNavigator: StackNavigator? // MARK: Initialization @@ -41,7 +39,7 @@ final class DebugMenuCoordinator: Coordinator, HasStackNavigator { /// - services: The services used by this coordinator. /// - stackNavigator: The stack navigator that is managed by this coordinator. /// - init( + public init( delegate: DebugMenuCoordinatorDelegate, services: Services, stackNavigator: StackNavigator, @@ -53,7 +51,7 @@ final class DebugMenuCoordinator: Coordinator, HasStackNavigator { // MARK: Methods - func navigate( + public func navigate( to route: DebugMenuRoute, context: AnyObject?, ) { @@ -66,7 +64,7 @@ final class DebugMenuCoordinator: Coordinator, HasStackNavigator { } /// Starts the process of displaying the debug menu. - func start() { + public func start() { showDebugMenu() } @@ -88,5 +86,5 @@ final class DebugMenuCoordinator: Coordinator, HasStackNavigator { // MARK: - HasErrorAlertServices extension DebugMenuCoordinator: HasErrorAlertServices { - var errorAlertServices: ErrorAlertServices { services } + public var errorAlertServices: ErrorAlertServices { services } } diff --git a/BitwardenShared/UI/Platform/DebugMenu/DebugMenuCoordinatorTests.swift b/BitwardenKit/UI/Platform/DebugMenu/DebugMenuCoordinatorTests.swift similarity index 79% rename from BitwardenShared/UI/Platform/DebugMenu/DebugMenuCoordinatorTests.swift rename to BitwardenKit/UI/Platform/DebugMenu/DebugMenuCoordinatorTests.swift index 341bd3407b..35d73fcc10 100644 --- a/BitwardenShared/UI/Platform/DebugMenu/DebugMenuCoordinatorTests.swift +++ b/BitwardenKit/UI/Platform/DebugMenu/DebugMenuCoordinatorTests.swift @@ -1,13 +1,11 @@ +import BitwardenKit import BitwardenKitMocks import SwiftUI import XCTest -@testable import BitwardenShared - class DebugMenuCoordinatorTests: BitwardenTestCase { // MARK: Properties - var appSettingsStore: MockAppSettingsStore! var configService: MockConfigService! var delegate: MockDebugMenuCoordinatorDelegate! var stackNavigator: MockStackNavigator! @@ -18,7 +16,6 @@ class DebugMenuCoordinatorTests: BitwardenTestCase { override func setUp() { super.setUp() - appSettingsStore = MockAppSettingsStore() configService = MockConfigService() delegate = MockDebugMenuCoordinatorDelegate() stackNavigator = MockStackNavigator() @@ -26,7 +23,6 @@ class DebugMenuCoordinatorTests: BitwardenTestCase { subject = DebugMenuCoordinator( delegate: delegate, services: ServiceContainer.withMocks( - appSettingsStore: appSettingsStore, configService: configService, ), stackNavigator: stackNavigator, @@ -36,7 +32,6 @@ class DebugMenuCoordinatorTests: BitwardenTestCase { override func tearDown() { super.tearDown() - appSettingsStore = nil configService = nil delegate = nil stackNavigator = nil @@ -45,6 +40,12 @@ class DebugMenuCoordinatorTests: BitwardenTestCase { // MARK: Tests + /// The coordinator has error alert services. + @MainActor + func test_errorAlertServices() { + XCTAssertNotNil(subject.errorAlertServices) + } + /// `navigate(to:)` with `.dismiss` dismisses the view. @MainActor func test_navigate_dismiss() throws { @@ -63,11 +64,3 @@ class DebugMenuCoordinatorTests: BitwardenTestCase { XCTAssertTrue(stackNavigator.actions.last?.view is DebugMenuView) } } - -class MockDebugMenuCoordinatorDelegate: DebugMenuCoordinatorDelegate { - var didDismissDebugMenuCalled = false - - func didDismissDebugMenu() { - didDismissDebugMenuCalled = true - } -} diff --git a/BitwardenShared/UI/Platform/DebugMenu/DebugMenuEffect.swift b/BitwardenKit/UI/Platform/DebugMenu/DebugMenuEffect.swift similarity index 96% rename from BitwardenShared/UI/Platform/DebugMenu/DebugMenuEffect.swift rename to BitwardenKit/UI/Platform/DebugMenu/DebugMenuEffect.swift index e76999ed8d..c31ad3e39a 100644 --- a/BitwardenShared/UI/Platform/DebugMenu/DebugMenuEffect.swift +++ b/BitwardenKit/UI/Platform/DebugMenu/DebugMenuEffect.swift @@ -1,5 +1,3 @@ -import Foundation - // MARK: - DebugMenuEffect /// Effects that can be processed by a `DebugMenuProcessor`. diff --git a/AuthenticatorShared/UI/DebugMenu/DebugMenuModule.swift b/BitwardenKit/UI/Platform/DebugMenu/DebugMenuModule.swift similarity index 60% rename from AuthenticatorShared/UI/DebugMenu/DebugMenuModule.swift rename to BitwardenKit/UI/Platform/DebugMenu/DebugMenuModule.swift index 3b6e7623e8..300b1e3d9b 100644 --- a/AuthenticatorShared/UI/DebugMenu/DebugMenuModule.swift +++ b/BitwardenKit/UI/Platform/DebugMenu/DebugMenuModule.swift @@ -5,26 +5,16 @@ import Foundation /// An object that builds coordinator for the debug menu. @MainActor -protocol DebugMenuModule { +public protocol DebugMenuModule { /// Initializes a coordinator for navigating between `DebugMenuRoute`s. /// /// - Parameters: + /// - delegate: The delegate for the debug menu coordinator. /// - stackNavigator: The stack navigator that will be used to navigate between routes. /// - Returns: A coordinator that can navigate to `DebugMenuRoute`s. /// func makeDebugMenuCoordinator( + delegate: DebugMenuCoordinatorDelegate, stackNavigator: StackNavigator, ) -> AnyCoordinator } - -extension DefaultAppModule: DebugMenuModule { - func makeDebugMenuCoordinator( - stackNavigator: StackNavigator, - ) -> AnyCoordinator { - DebugMenuCoordinator( - services: services, - stackNavigator: stackNavigator, - ) - .asAnyCoordinator() - } -} diff --git a/BitwardenShared/UI/Platform/DebugMenu/DebugMenuProcessor.swift b/BitwardenKit/UI/Platform/DebugMenu/DebugMenuProcessor.swift similarity index 68% rename from BitwardenShared/UI/Platform/DebugMenu/DebugMenuProcessor.swift rename to BitwardenKit/UI/Platform/DebugMenu/DebugMenuProcessor.swift index 6b54258431..f71734901f 100644 --- a/BitwardenShared/UI/Platform/DebugMenu/DebugMenuProcessor.swift +++ b/BitwardenKit/UI/Platform/DebugMenu/DebugMenuProcessor.swift @@ -1,4 +1,3 @@ -import BitwardenKit import BitwardenSdk import Foundation @@ -20,6 +19,18 @@ final class DebugMenuProcessor: StateProcessor Element? { - guard index >= 0, index < endIndex else { - return nil - } - - return self[index] - } -} diff --git a/BitwardenShared/UI/Platform/Application/AppModule.swift b/BitwardenShared/UI/Platform/Application/AppModule.swift index fa465cd51d..839605968e 100644 --- a/BitwardenShared/UI/Platform/Application/AppModule.swift +++ b/BitwardenShared/UI/Platform/Application/AppModule.swift @@ -64,6 +64,22 @@ extension DefaultAppModule: AppModule { } } +// MARK: - DefaultAppModule + DebugMenuModule + +extension DefaultAppModule: DebugMenuModule { + public func makeDebugMenuCoordinator( + delegate: DebugMenuCoordinatorDelegate, + stackNavigator: StackNavigator, + ) -> AnyCoordinator { + DebugMenuCoordinator( + delegate: delegate, + services: services, + stackNavigator: stackNavigator, + ) + .asAnyCoordinator() + } +} + // MARK: - DefaultAppModule + FlightRecorderModule extension DefaultAppModule: FlightRecorderModule { diff --git a/BitwardenShared/UI/Platform/DebugMenu/DebugMenuModule.swift b/BitwardenShared/UI/Platform/DebugMenu/DebugMenuModule.swift deleted file mode 100644 index 104090dd7e..0000000000 --- a/BitwardenShared/UI/Platform/DebugMenu/DebugMenuModule.swift +++ /dev/null @@ -1,34 +0,0 @@ -import BitwardenKit -import Foundation - -// MARK: - DebugMenuModule - -/// An object that builds coordinator for the debug menu. -@MainActor -protocol DebugMenuModule { - /// Initializes a coordinator for navigating between `DebugMenuRoute`s. - /// - /// - Parameters: - /// - delegate: The delegate for the debug menu coordinator. - /// - stackNavigator: The stack navigator that will be used to navigate between routes. - /// - Returns: A coordinator that can navigate to `DebugMenuRoute`s. - /// - func makeDebugMenuCoordinator( - delegate: DebugMenuCoordinatorDelegate, - stackNavigator: StackNavigator, - ) -> AnyCoordinator -} - -extension DefaultAppModule: DebugMenuModule { - func makeDebugMenuCoordinator( - delegate: DebugMenuCoordinatorDelegate, - stackNavigator: StackNavigator, - ) -> AnyCoordinator { - DebugMenuCoordinator( - delegate: delegate, - services: services, - stackNavigator: stackNavigator, - ) - .asAnyCoordinator() - } -} diff --git a/BitwardenShared/UI/Platform/DebugMenu/DebugMenuRoute.swift b/BitwardenShared/UI/Platform/DebugMenu/DebugMenuRoute.swift deleted file mode 100644 index 8c8dc04d0c..0000000000 --- a/BitwardenShared/UI/Platform/DebugMenu/DebugMenuRoute.swift +++ /dev/null @@ -1,9 +0,0 @@ -import Foundation - -// MARK: - DebugMenuRoute - -/// A route to specific screens in the` DebugMenuView` -public enum DebugMenuRoute: Equatable, Hashable { - /// A route to dismiss the screen currently presented modally. - case dismiss -} diff --git a/GlobalTestHelpers-bwa/MockAppModule.swift b/GlobalTestHelpers-bwa/MockAppModule.swift index 3dd365dfd3..146b78b9b4 100644 --- a/GlobalTestHelpers-bwa/MockAppModule.swift +++ b/GlobalTestHelpers-bwa/MockAppModule.swift @@ -23,6 +23,7 @@ class MockAppModule: var authRouter = MockRouter(routeForEvent: { _ in .vaultUnlock }) var authenticatorItemCoordinator = MockCoordinator() var debugMenuCoordinator = MockCoordinator() + var debugMenuCoordinatorDelegate: DebugMenuCoordinatorDelegate? var fileSelectionDelegate: FileSelectionDelegate? var fileSelectionCoordinator = MockCoordinator() var flightRecorderCoordinator = MockCoordinator() @@ -61,9 +62,11 @@ class MockAppModule: } func makeDebugMenuCoordinator( + delegate: DebugMenuCoordinatorDelegate, stackNavigator: StackNavigator, ) -> AnyCoordinator { - debugMenuCoordinator.asAnyCoordinator() + debugMenuCoordinatorDelegate = delegate + return debugMenuCoordinator.asAnyCoordinator() } func makeFileSelectionCoordinator( diff --git a/project-bwa.yml b/project-bwa.yml index a28738686e..57f4e21925 100644 --- a/project-bwa.yml +++ b/project-bwa.yml @@ -260,6 +260,7 @@ targets: - target: BitwardenKit/AuthenticatorBridgeKitMocks - target: BitwardenKit/BitwardenKitMocks - target: BitwardenKit/TestHelpers + - package: BitwardenSdk - package: SnapshotTesting product: InlineSnapshotTesting randomExecutionOrder: true @@ -284,6 +285,7 @@ targets: - target: BitwardenKit/AuthenticatorBridgeKitMocks - target: BitwardenKit/BitwardenKitMocks - target: BitwardenKit/TestHelpers + - package: BitwardenSdk - package: SnapshotTesting - package: SnapshotTesting product: InlineSnapshotTesting @@ -310,5 +312,6 @@ targets: - target: BitwardenKit/BitwardenKitMocks - target: BitwardenKit/TestHelpers - target: BitwardenKit/ViewInspectorTestHelpers + - package: BitwardenSdk - package: ViewInspector randomExecutionOrder: true diff --git a/project-bwk.yml b/project-bwk.yml index 0ca47904d4..e5e39a45fe 100644 --- a/project-bwk.yml +++ b/project-bwk.yml @@ -141,6 +141,7 @@ targets: - "**/sourcery.yml" buildPhase: none dependencies: + - package: BitwardenSdk - package: SwiftUIIntrospect - target: BitwardenResources - target: Networking @@ -190,6 +191,7 @@ targets: - target: BitwardenKit - target: BitwardenKitMocks - target: TestHelpers + - package: BitwardenSdk - package: SnapshotTesting product: InlineSnapshotTesting randomExecutionOrder: true diff --git a/project-pm.yml b/project-pm.yml index e86b457061..1cf30463c9 100644 --- a/project-pm.yml +++ b/project-pm.yml @@ -181,6 +181,7 @@ targets: - target: BitwardenKit/BitwardenKit - target: BitwardenKit/BitwardenResources - target: BitwardenKit/Networking + - package: BitwardenSdk - package: Firebase product: FirebaseCrashlytics preBuildScripts: @@ -244,6 +245,7 @@ targets: - "**/*Tests.*" - "**/TestHelpers/*" dependencies: + - package: BitwardenSdk - target: BitwardenShared BitwardenActionExtensionTests: type: bundle.unit-test @@ -279,6 +281,7 @@ targets: - "**/*Tests.*" - "**/TestHelpers/*" dependencies: + - package: BitwardenSdk - target: BitwardenShared BitwardenAutoFillExtensionTests: type: bundle.unit-test @@ -314,6 +317,7 @@ targets: - "**/*Tests.*" - "**/TestHelpers/*" dependencies: + - package: BitwardenSdk - target: BitwardenShared BitwardenShareExtensionTests: type: bundle.unit-test @@ -422,6 +426,7 @@ targets: - target: BitwardenKit/AuthenticatorBridgeKitMocks - target: BitwardenKit/BitwardenKitMocks - target: BitwardenKit/TestHelpers + - package: BitwardenSdk - package: SnapshotTesting product: InlineSnapshotTesting randomExecutionOrder: true @@ -452,6 +457,7 @@ targets: - target: BitwardenShared - target: BitwardenKit/BitwardenKitMocks - target: BitwardenKit/TestHelpers + - package: BitwardenSdk - package: SnapshotTesting - package: SnapshotTesting product: InlineSnapshotTesting @@ -484,6 +490,7 @@ targets: - target: BitwardenKit/BitwardenKitMocks - target: BitwardenKit/TestHelpers - target: BitwardenKit/ViewInspectorTestHelpers + - package: BitwardenSdk - package: ViewInspector randomExecutionOrder: true