Skip to content

Commit

Permalink
[BWA-85] Add debug screen (#166)
Browse files Browse the repository at this point in the history
  • Loading branch information
KatherineInCode authored Oct 30, 2024
1 parent 40f90b8 commit 83e8934
Show file tree
Hide file tree
Showing 44 changed files with 1,636 additions and 75 deletions.
4 changes: 2 additions & 2 deletions .swiftlint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,8 @@ custom_rules:
severity: warning
todo_without_jira:
name: "TODO without JIRA"
regex: "(TODO|TO DO|FIX|FIXME|FIX ME|todo)(?!: BIT-[0-9]{1,})" # "TODO: BIT-123"
message: "All TODOs must be followed by a JIRA reference, for example: \"TODO: BIT-123\""
regex: "(TODO|TO DO|FIX|FIXME|FIX ME|todo)(?!: BWA-[0-9]{1,})" # "TODO: BWA-123"
message: "All TODOs must be followed by a JIRA reference, for example: \"TODO: BWA-123\""
match_kinds:
- comment
severity: warning
Expand Down
34 changes: 32 additions & 2 deletions Authenticator/Application/SceneDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ import UIKit
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
// MARK: Properties

/// The processor that manages application level logic.
var appProcessor: AppProcessor? {
(UIApplication.shared.delegate as? AppDelegateType)?.appProcessor
}

/// Whether the app is still starting up. This ensures the splash view isn't dismissed on start
/// up until the processor has shown the initial view.
var isStartingUp = true
Expand All @@ -24,7 +29,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
options connectionOptions: UIScene.ConnectionOptions
) {
guard let windowScene = scene as? UIWindowScene else { return }
guard let appProcessor = (UIApplication.shared.delegate as? AppDelegateType)?.appProcessor else {
guard let appProcessor else {
if (UIApplication.shared.delegate as? AppDelegateType)?.isTesting == true {
// If the app is running tests, show a testing view.
window = buildSplashWindow(windowScene: windowScene)
Expand All @@ -34,7 +39,11 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
}

let rootViewController = RootViewController()
let appWindow = UIWindow(windowScene: windowScene)
let appWindow = ShakeWindow(windowScene: windowScene) { [weak self] in
#if DEBUG_MENU
self?.appProcessor?.showDebugMenu()
#endif
}
appWindow.rootViewController = rootViewController
appWindow.makeKeyAndVisible()
window = appWindow
Expand Down Expand Up @@ -92,4 +101,25 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
private func showSplash() {
splashWindow?.alpha = 1
}

#if DEBUG_MENU
/// Handle the triple-tap gesture and launch the debug menu.
@objc
private func handleTripleTapGesture() {
appProcessor?.showDebugMenu()
}
#endif

#if DEBUG_MENU
/// Add the triple-tap gesture recognizer to the window.
private func addTripleTapGestureRecognizer(to window: UIWindow) {
let tapGesture = UITapGestureRecognizer(
target: self,
action: #selector(handleTripleTapGesture)
)
tapGesture.numberOfTapsRequired = 3
tapGesture.numberOfTouchesRequired = 1
window.addGestureRecognizer(tapGesture)
}
#endif
}
Original file line number Diff line number Diff line change
Expand Up @@ -151,8 +151,8 @@ class DefaultKeychainRepository: KeychainRepository {
)

if let resultDictionary = foundItem as? [String: Any],
let data = resultDictionary[kSecValueData as String] as? Data {
let string = String(decoding: data, as: UTF8.self)
let data = resultDictionary[kSecValueData as String] as? Data,
let string = String(data: data, encoding: .utf8) {
guard !string.isEmpty else {
throw KeychainServiceError.keyNotFound(item)
}
Expand Down
91 changes: 91 additions & 0 deletions AuthenticatorShared/Core/Platform/Models/Domain/ServerConfig.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import Foundation

// MARK: - ServerConfig

/// Model that represents the configuration provided by the server at a particular time.
///
struct ServerConfig: Equatable, Codable, Sendable {
// MARK: Properties

/// The environment URLs of the server.
let environment: EnvironmentServerConfig?

/// The particular time of the server configuration.
let date: Date

/// Feature flags to configure the client.
let featureStates: [FeatureFlag: AnyCodable]

/// The git hash of the server.
let gitHash: String

/// Third party server information.
let server: ThirdPartyServerConfig?

/// The version of the server.
let version: String

init(date: Date, responseModel: ConfigResponseModel) {
environment = responseModel.environment.map(EnvironmentServerConfig.init)
self.date = date
let features: [(FeatureFlag, AnyCodable)]
features = responseModel.featureStates.compactMap { key, value in
guard let flag = FeatureFlag(rawValue: key) else { return nil }
return (flag, value)
}
featureStates = Dictionary(uniqueKeysWithValues: features)

gitHash = responseModel.gitHash
server = responseModel.server.map(ThirdPartyServerConfig.init)
version = responseModel.version
}
}

// MARK: - ThirdPartyServerConfig

/// Model for third-party configuration of the server.
///
struct ThirdPartyServerConfig: Equatable, Codable {
/// The name of the third party configuration.
let name: String

/// The URL of the third party configuration.
let url: String

init(responseModel: ThirdPartyConfigResponseModel) {
name = responseModel.name
url = responseModel.url
}
}

// MARK: - EnvironmentServerConfig

/// Model for the environment URLs in a server configuration.
struct EnvironmentServerConfig: Equatable, Codable {
/// The API URL.
let api: String?

/// The Cloud Region (e.g. "US")
let cloudRegion: String?

/// The Identity URL.
let identity: String?

/// The Notifications URL.
let notifications: String?

/// The SSO URL.
let sso: String?

/// The Vault URL.
let vault: String?

init(responseModel: EnvironmentServerConfigResponseModel) {
api = responseModel.api
cloudRegion = responseModel.cloudRegion
identity = responseModel.identity
notifications = responseModel.notifications
sso = responseModel.sso
vault = responseModel.vault
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import Foundation
import XCTest

@testable import AuthenticatorShared

final class ServerConfigTests: AuthenticatorTestCase {
// MARK: Tests

/// `init` properly converts feature flags
func test_init_featureFlags() {
let model = ConfigResponseModel(
environment: nil,
featureStates: [
"vault-onboarding": .bool(true),
"test-remote-feature-flag": .bool(false),
"not-a-real-feature-flag": .int(42),
],
gitHash: "123",
server: nil,
version: "1.2.3"
)

let subject = ServerConfig(date: Date(), responseModel: model)
XCTAssertEqual(subject.featureStates, [.testRemoteFeatureFlag: .bool(false)])
}
}
74 changes: 59 additions & 15 deletions AuthenticatorShared/Core/Platform/Models/Enum/FeatureFlag.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,36 +4,80 @@ import Foundation

/// An enum to represent a feature flag sent by the server
///
enum FeatureFlag: String, Codable {
enum FeatureFlag: String, CaseIterable, Codable {
// MARK: Feature Flags

/// A feature flag that determines whether or not the password manager sync capability is enabled.
case enablePasswordManagerSync = "enable-password-manager-sync-ios"

// MARK: Test Flags

/// A test feature flag that has a local boolean default.
case testLocalBoolFlag = "test-local-bool-flag"
/// A test feature flag that isn't remotely configured and has no initial value.
case testLocalFeatureFlag = "test-local-feature-flag"

/// A test feature flag that has a local integer default.
case testLocalIntFlag = "test-local-int-flag"
/// A test feature flag that has an initial boolean value and is not remotely configured.
case testLocalInitialBoolFlag = "test-local-initial-bool-flag"

/// A test feature flag that has a local string default.
case testLocalStringFlag = "test-local-string-flag"
/// A test feature flag that has an initial integer value and is not remotely configured.
case testLocalInitialIntFlag = "test-local-initial-int-flag"

/// A test feature flag to represent a value that doesn't have a local default.
case testRemoteFlag
/// A test feature flag that has an initial string value and is not remotely configured.
case testLocalInitialStringFlag = "test-local-initial-string-flag"

// MARK: Static Properties
/// A test feature flag that can be remotely configured.
case testRemoteFeatureFlag = "test-remote-feature-flag"

/// The values to start the value for each flag at locally.
/// A test feature flag that has an initial boolean value and is not remotely configured.
case testRemoteInitialBoolFlag = "test-remote-initial-bool-flag"

/// A test feature flag that has an initial integer value and is not remotely configured.
case testRemoteInitialIntFlag = "test-remote-initial-int-flag"

/// A test feature flag that has an initial string value and is not remotely configured.
case testRemoteInitialStringFlag = "test-remote-initial-string-flag"

// MARK: Type Properties

/// An array of feature flags available in the debug menu.
static var debugMenuFeatureFlags: [FeatureFlag] {
allCases.filter { !$0.rawValue.hasPrefix("test-") }
}

/// The initial values for feature flags.
/// If `isRemotelyConfigured` is true for the flag, then this will get overridden by the server;
/// but if `isRemotelyConfigured` is false for the flag, then the value here will be used.
/// This is a helpful way to manage local feature flags.
static let initialLocalValues: [FeatureFlag: AnyCodable] = [
static let initialValues: [FeatureFlag: AnyCodable] = [
.enablePasswordManagerSync: .bool(false),
.testLocalBoolFlag: .bool(true),
.testLocalIntFlag: .int(42),
.testLocalStringFlag: .string("Test String"),
.testLocalInitialBoolFlag: .bool(true),
.testLocalInitialIntFlag: .int(42),
.testLocalInitialStringFlag: .string("Test String"),
.testRemoteInitialBoolFlag: .bool(true),
.testRemoteInitialIntFlag: .int(42),
.testRemoteInitialStringFlag: .string("Test String"),
]

// MARK: Instance Properties

/// Whether this feature can be enabled remotely.
var isRemotelyConfigured: Bool {
switch self {
case .enablePasswordManagerSync,
.testLocalFeatureFlag,
.testLocalInitialBoolFlag,
.testLocalInitialIntFlag,
.testLocalInitialStringFlag:
false
case .testRemoteFeatureFlag,
.testRemoteInitialBoolFlag,
.testRemoteInitialIntFlag,
.testRemoteInitialStringFlag:
true
}
}

/// The display name of the feature flag.
var name: String {
rawValue.split(separator: "-").map(\.localizedCapitalized).joined(separator: " ")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import XCTest

@testable import AuthenticatorShared

final class FeatureFlagTests: AuthenticatorTestCase {
// MARK: Tests

/// `debugMenuFeatureFlags` does not include any test flags
func test_debugMenu_testFlags() {
let actual = FeatureFlag.debugMenuFeatureFlags.map(\.rawValue)
let filtered = actual.filter { $0.hasPrefix("test-") }
XCTAssertEqual(filtered, [])
}

/// `name` formats the raw value of a feature flag
func test_name() {
XCTAssertEqual(FeatureFlag.testLocalFeatureFlag.name, "Test Local Feature Flag")
XCTAssertEqual(FeatureFlag.testLocalInitialBoolFlag.name, "Test Local Initial Bool Flag")
XCTAssertEqual(FeatureFlag.testLocalInitialIntFlag.name, "Test Local Initial Int Flag")
XCTAssertEqual(FeatureFlag.testLocalInitialStringFlag.name, "Test Local Initial String Flag")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import Foundation
import Networking

// MARK: - ConfigResponseModel

/// API response model for the configuration request.
///
struct ConfigResponseModel: Equatable, JSONResponse {
// MARK: Properties

/// The environment URLs of the server.
let environment: EnvironmentServerConfigResponseModel?

/// Feature flags to configure the client.
let featureStates: [String: AnyCodable]

/// The git hash of the server.
let gitHash: String

/// Third party server information.
let server: ThirdPartyConfigResponseModel?

/// The version of the server.
let version: String
}

/// API response model for third-party configuration in a configuration response.
struct ThirdPartyConfigResponseModel: Equatable, JSONResponse {
/// The name of the third party configuration.
let name: String

/// The URL of the third party configuration.
let url: String
}

/// API response model for the environment URLs in a configuration response.
struct EnvironmentServerConfigResponseModel: Equatable, JSONResponse {
/// The API URL.
let api: String?

/// The Cloud Region (e.g. "US")
let cloudRegion: String?

/// The Identity URL.
let identity: String?

/// The Notifications URL.
let notifications: String?

/// The SSO URL.
let sso: String?

/// The Vault URL.
let vault: String?
}
Loading

0 comments on commit 83e8934

Please sign in to comment.