diff --git a/mobile/Verify/Core/EnrollClient.swift b/mobile/Verify/Core/EnrollClient.swift index ef4565b2b24..bb74f2ad9f9 100644 --- a/mobile/Verify/Core/EnrollClient.swift +++ b/mobile/Verify/Core/EnrollClient.swift @@ -34,7 +34,6 @@ public enum EnrollClientError: Error, Sendable { case clientCreationFailed } - extension EnrollClient { public static let liveValue = EnrollClient( requestEnrollmentToken: { hostName, port, pairingToken in @@ -53,4 +52,3 @@ extension EnrollClient { }, ) } - diff --git a/mobile/Verify/Verify.xcodeproj/project.pbxproj b/mobile/Verify/Verify.xcodeproj/project.pbxproj index a8da099befc..a3025139c1c 100644 --- a/mobile/Verify/Verify.xcodeproj/project.pbxproj +++ b/mobile/Verify/Verify.xcodeproj/project.pbxproj @@ -266,7 +266,7 @@ attributes = { BuildIndependentTargetsInParallel = 1; LastSwiftUpdateCheck = 2650; - LastUpgradeCheck = 2650; + LastUpgradeCheck = 2700; TargetAttributes = { 0C94C8512FA232E600C13C9A = { CreatedOnToolsVersion = 26.4.1; @@ -651,6 +651,7 @@ isa = XCBuildConfiguration; buildSettings = { BUILD_LIBRARY_FOR_DISTRIBUTION = YES; + CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = K497G57PDJ; @@ -706,6 +707,7 @@ isa = XCBuildConfiguration; buildSettings = { BUILD_LIBRARY_FOR_DISTRIBUTION = YES; + CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = K497G57PDJ; diff --git a/mobile/Verify/Verify.xcodeproj/xcshareddata/xcschemes/Verify (Fresh Database).xcscheme b/mobile/Verify/Verify.xcodeproj/xcshareddata/xcschemes/Verify (Fresh Database).xcscheme index 09dccff1599..2796179be4c 100644 --- a/mobile/Verify/Verify.xcodeproj/xcshareddata/xcschemes/Verify (Fresh Database).xcscheme +++ b/mobile/Verify/Verify.xcodeproj/xcshareddata/xcschemes/Verify (Fresh Database).xcscheme @@ -1,6 +1,6 @@ String { + func chunk() -> String { + (0 ..< 4) + .compactMap { _ in Self.base.randomElement() } + .reduce(into: "") { $0.append($1) } + } + return "\(chunk())-\(chunk())" + } + } +#endif diff --git a/mobile/Verify/Verify/Device Trust/Serial Number/SerialNumberClient.swift b/mobile/Verify/Verify/Device Trust/Serial Number/SerialNumberClient.swift new file mode 100644 index 00000000000..6353f833e32 --- /dev/null +++ b/mobile/Verify/Verify/Device Trust/Serial Number/SerialNumberClient.swift @@ -0,0 +1,44 @@ +// Teleport +// Copyright (C) 2026 Gravitational, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see http://www.gnu.org/licenses/ + +import DependenciesMacros +import Foundation +import Sharing + +@DependencyClient +struct SerialNumberClient { + var getDeviceSerialNumber: @Sendable () -> String = { "" } +} + +extension SerialNumberClient { + static let liveValue = SerialNumberClient( + getDeviceSerialNumber: { + #if DEBUG + @Shared(.debugStorage(.debugSerialNumber)) + var debugSerialNumber: String? = nil + + guard let debugSerialNumber else { + let newDebugSerialNumber = FakeSerialNumberGenerator.generate() + $debugSerialNumber.withLock { $0 = newDebugSerialNumber } + return newDebugSerialNumber + } + return debugSerialNumber + #else + fatalError("Production path has not yet been implemented. Requires messing around in Jamf.") + #endif + }, + ) +} diff --git a/mobile/Verify/Verify/Features/Debugging/Debug Settings/DebugSettingsView.swift b/mobile/Verify/Verify/Features/Debugging/Debug Settings/DebugSettingsView.swift new file mode 100644 index 00000000000..da3af5e0172 --- /dev/null +++ b/mobile/Verify/Verify/Features/Debugging/Debug Settings/DebugSettingsView.swift @@ -0,0 +1,43 @@ +// Teleport +// Copyright (C) 2026 Gravitational, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see http://www.gnu.org/licenses/ + +#if DEBUG + + import SwiftUI + + struct DebugSettingsView: View { + @Bindable + var viewModel: DebugSettingsViewModel + + var body: some View { + Form { + Section("Serial Number") { + HStack { + TextField("Serial Number", text: viewModel.debugSerialNumberBinding) + .textFieldStyle(.roundedBorder) + .frame(maxWidth: .infinity, alignment: .leading) + Button { + viewModel.regenerateSerialNumber() + } label: { + Image(systemName: "arrow.circlepath") + } + } + } + } + } + } + +#endif diff --git a/mobile/Verify/Verify/Features/Debugging/Debug Settings/DebugSettingsViewModel.swift b/mobile/Verify/Verify/Features/Debugging/Debug Settings/DebugSettingsViewModel.swift new file mode 100644 index 00000000000..7ab0367b67e --- /dev/null +++ b/mobile/Verify/Verify/Features/Debugging/Debug Settings/DebugSettingsViewModel.swift @@ -0,0 +1,47 @@ +// Teleport +// Copyright (C) 2026 Gravitational, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see http://www.gnu.org/licenses/ + +#if DEBUG + + import Dependencies + import Foundation + import Sharing + import SwiftUI + + @Observable @MainActor + final class DebugSettingsViewModel { + @ObservationIgnored + @Dependency(\.serialNumberClient) + var serialNumberClient + + @ObservationIgnored + @Shared(.debugStorage(.debugSerialNumber)) + var debugSerialNumber: String? = nil + } + + // MARK: - Serial Number + + extension DebugSettingsViewModel { + var debugSerialNumberBinding: Binding { + Binding($debugSerialNumber.emptyIfNil) + } + + func regenerateSerialNumber() { + $debugSerialNumber.withLock { $0 = FakeSerialNumberGenerator.generate() } + } + } + +#endif diff --git a/mobile/Verify/Verify/Features/Debugging/DebugView.swift b/mobile/Verify/Verify/Features/Debugging/DebugView.swift index 43ca8dc04cb..ab12818869d 100644 --- a/mobile/Verify/Verify/Features/Debugging/DebugView.swift +++ b/mobile/Verify/Verify/Features/Debugging/DebugView.swift @@ -30,11 +30,14 @@ var body: some View { NavigationStack { List { - Group { - Section("Feature Demos") { - Button("Secure Enclave Storage", systemImage: "lock") { - viewModel.destination = .deviceTrustCredentialDemo(.init()) - } + Section("Settings") { + Button("Debug Settings", systemImage: "gearshape") { + viewModel.destination = .debugSettingsView(.init()) + } + } + Section("Feature Demos") { + Button("Secure Enclave Storage", systemImage: "lock") { + viewModel.destination = .deviceTrustCredentialDemo(.init()) } } } @@ -45,6 +48,9 @@ .navigationDestination(item: $viewModel.destination.deviceTrustCredentialDemo) { viewModel in FeatureDemo.DeviceTrustCredentialView(viewModel: viewModel) } + .navigationDestination(item: $viewModel.destination.debugSettingsView) { viewModel in + DebugSettingsView(viewModel: viewModel) + } } } } diff --git a/mobile/Verify/Verify/Features/Debugging/DebugViewModel.swift b/mobile/Verify/Verify/Features/Debugging/DebugViewModel.swift index 03f59943bdb..7affa470c2c 100644 --- a/mobile/Verify/Verify/Features/Debugging/DebugViewModel.swift +++ b/mobile/Verify/Verify/Features/Debugging/DebugViewModel.swift @@ -23,6 +23,10 @@ final class DebugViewModel { @CasePathable enum Destination { + // MARK: - Debug Settings + + case debugSettingsView(DebugSettingsViewModel) + // MARK: - Feature Demos case deviceTrustCredentialDemo(FeatureDemo.DeviceTrustCredentialViewModel) diff --git a/mobile/Verify/Verify/Features/Landing/LandingView.swift b/mobile/Verify/Verify/Features/Landing/LandingView.swift index 8f7e5f931ad..ae5f034271b 100644 --- a/mobile/Verify/Verify/Features/Landing/LandingView.swift +++ b/mobile/Verify/Verify/Features/Landing/LandingView.swift @@ -47,7 +47,7 @@ struct LandingView: View { .background(Color.Background.depth3) // MARK: Toolbar - + .toolbarVisibility(viewModel.shouldShowToolbar ? .visible : .hidden) .toolbar { ToolbarItem { @@ -101,7 +101,6 @@ struct LandingView: View { } #endif // swiftformat:enable indent - } } } diff --git a/mobile/Verify/Verify/Persistence/AppDatabase+Migrations.swift b/mobile/Verify/Verify/Persistence/SQLite/AppDatabase+Migrations.swift similarity index 100% rename from mobile/Verify/Verify/Persistence/AppDatabase+Migrations.swift rename to mobile/Verify/Verify/Persistence/SQLite/AppDatabase+Migrations.swift diff --git a/mobile/Verify/Verify/Persistence/AppDatabase.swift b/mobile/Verify/Verify/Persistence/SQLite/AppDatabase.swift similarity index 100% rename from mobile/Verify/Verify/Persistence/AppDatabase.swift rename to mobile/Verify/Verify/Persistence/SQLite/AppDatabase.swift diff --git a/mobile/Verify/Verify/Persistence/Models/Cluster.swift b/mobile/Verify/Verify/Persistence/SQLite/Models/Cluster.swift similarity index 100% rename from mobile/Verify/Verify/Persistence/Models/Cluster.swift rename to mobile/Verify/Verify/Persistence/SQLite/Models/Cluster.swift diff --git a/mobile/Verify/Verify/Persistence/UserDefaults/UserDefaults+Debug.swift b/mobile/Verify/Verify/Persistence/UserDefaults/UserDefaults+Debug.swift new file mode 100644 index 00000000000..671ded50188 --- /dev/null +++ b/mobile/Verify/Verify/Persistence/UserDefaults/UserDefaults+Debug.swift @@ -0,0 +1,52 @@ +// Teleport +// Copyright (C) 2026 Gravitational, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see http://www.gnu.org/licenses/ + +#if DEBUG + import Foundation + import Sharing + + extension UserDefaults { + static let debugSuiteName = "com.gravitational.teleport.verify.debug" + + /// A UserDefaults suite that's only available in debug builds. + /// + /// This is marked as `nonisolated(unsafe)` because UserDefaults isn't marked Sendable, but it _is_ thread safe. + /// See + nonisolated(unsafe) static let debug: UserDefaults = { + guard let store = UserDefaults(suiteName: debugSuiteName) else { + preconditionFailure("Could not instantiate debug user defaults.") + } + return store + }() + } + + // MARK: - Keys + + /// Type-safe keys for use when querying the debug user defaults + enum DebugUserDefaultsKey: String { + case debugSerialNumber + } + + /// A small helper function to access the debug store via the Sharing library + extension SharedReaderKey where Self == AppStorageKey { + static func debugStorage( + _ key: DebugUserDefaultsKey, + ) -> Self { + .appStorage(key.rawValue, store: .debug) + } + } + +#endif diff --git a/mobile/Verify/Verify/Utilities/String+Helpers.swift b/mobile/Verify/Verify/Utilities/String+Helpers.swift new file mode 100644 index 00000000000..8a93d8e3d67 --- /dev/null +++ b/mobile/Verify/Verify/Utilities/String+Helpers.swift @@ -0,0 +1,29 @@ +// Teleport +// Copyright (C) 2026 Gravitational, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see http://www.gnu.org/licenses/ + +import Foundation + +extension String? { + var emptyIfNil: String { + get { + guard let self else { return "" } + return self + } + set { + self = newValue + } + } +}