diff --git a/TestHarnessShared/Core/Autofill/Passkey/CipherStorageService.swift b/TestHarnessShared/Core/Autofill/Passkey/CipherStorageService.swift index 5924fbb1df..184d43455f 100644 --- a/TestHarnessShared/Core/Autofill/Passkey/CipherStorageService.swift +++ b/TestHarnessShared/Core/Autofill/Passkey/CipherStorageService.swift @@ -4,7 +4,7 @@ import Foundation // MARK: - CipherStorageService // sourcery: AutoMockable -/// A service that locally persists the SDK-encrypted `Cipher`s created by the SDK-backed passkey +/// A service that locally persists the SDK-encrypted `Cipher`s created by the passkey /// scenarios, so they survive app relaunches. /// protocol CipherStorageService: AnyObject { diff --git a/TestHarnessShared/Core/Autofill/Passkey/ClientManagedTokensProvider.swift b/TestHarnessShared/Core/Autofill/Passkey/ClientManagedTokensProvider.swift index 4d74aa11b4..a4f03bbc76 100644 --- a/TestHarnessShared/Core/Autofill/Passkey/ClientManagedTokensProvider.swift +++ b/TestHarnessShared/Core/Autofill/Passkey/ClientManagedTokensProvider.swift @@ -2,7 +2,7 @@ import BitwardenSdk // MARK: - ClientManagedTokensProvider -/// A `ClientManagedTokens` implementation for the SDK-backed passkey scenarios, which never +/// A `ClientManagedTokens` implementation for the passkey scenarios, which never /// make network requests and so never have an access token to provide. /// final class ClientManagedTokensProvider: ClientManagedTokens { diff --git a/TestHarnessShared/Core/Autofill/Passkey/DefaultFido2CredentialStore.swift b/TestHarnessShared/Core/Autofill/Passkey/DefaultFido2CredentialStore.swift index a041955f37..15172f7a2c 100644 --- a/TestHarnessShared/Core/Autofill/Passkey/DefaultFido2CredentialStore.swift +++ b/TestHarnessShared/Core/Autofill/Passkey/DefaultFido2CredentialStore.swift @@ -4,7 +4,7 @@ import Foundation // MARK: - DefaultFido2CredentialStore -/// A `Fido2CredentialStore` for the SDK-backed passkey scenarios, backed by an injected +/// A `Fido2CredentialStore` for the passkey scenarios, backed by an injected /// `CipherStorageService` so credentials survive app relaunches as long as the same synthetic /// identity — and therefore the same crypto keys — is reconstructed alongside it. /// diff --git a/TestHarnessShared/Core/Autofill/Passkey/DefaultFido2UserInterface.swift b/TestHarnessShared/Core/Autofill/Passkey/DefaultFido2UserInterface.swift index 5696503d5e..0ea33eab26 100644 --- a/TestHarnessShared/Core/Autofill/Passkey/DefaultFido2UserInterface.swift +++ b/TestHarnessShared/Core/Autofill/Passkey/DefaultFido2UserInterface.swift @@ -3,7 +3,7 @@ import Foundation // MARK: - DefaultFido2UserInterface -/// A minimal `Fido2UserInterface` implementation for the SDK-backed passkey scenarios. There's +/// A minimal `Fido2UserInterface` implementation for the passkey scenarios. There's /// no vault, biometrics, or master password reprompt to mediate here, so this always approves /// user checks and synthesizes a single throwaway login item for new credentials. /// diff --git a/TestHarnessShared/Core/Autofill/Passkey/Fixtures/MakeCredentialResult+Fixtures.swift b/TestHarnessShared/Core/Autofill/Passkey/Fixtures/MakeCredentialResult+Fixtures.swift new file mode 100644 index 0000000000..419c011ea3 --- /dev/null +++ b/TestHarnessShared/Core/Autofill/Passkey/Fixtures/MakeCredentialResult+Fixtures.swift @@ -0,0 +1,17 @@ +import BitwardenSdk +import Foundation + +extension MakeCredentialResult { + static func fixture( + attestationObject: Data = Data([0x01]), + authenticatorData: Data = Data([0x02]), + credentialId: Data = Data([0x03]), + ) -> MakeCredentialResult { + MakeCredentialResult( + authenticatorData: authenticatorData, + attestationObject: attestationObject, + credentialId: credentialId, + extensions: MakeCredentialExtensionsOutput(prf: nil), + ) + } +} diff --git a/TestHarnessShared/Core/Autofill/Passkey/PasskeyError.swift b/TestHarnessShared/Core/Autofill/Passkey/PasskeyError.swift index e8c9345432..a93d1ad485 100644 --- a/TestHarnessShared/Core/Autofill/Passkey/PasskeyError.swift +++ b/TestHarnessShared/Core/Autofill/Passkey/PasskeyError.swift @@ -2,7 +2,7 @@ import Foundation // MARK: - PasskeyError -/// Errors produced by the SDK-backed passkey scenarios. +/// Errors produced by the passkey scenarios. /// enum PasskeyError: Equatable, Error, LocalizedError { /// More than one stored credential matched the requested relying party; picking between diff --git a/TestHarnessShared/Core/Autofill/Passkey/PasskeyKeychainItem.swift b/TestHarnessShared/Core/Autofill/Passkey/PasskeyKeychainItem.swift index 75a22417f4..84b3dd696e 100644 --- a/TestHarnessShared/Core/Autofill/Passkey/PasskeyKeychainItem.swift +++ b/TestHarnessShared/Core/Autofill/Passkey/PasskeyKeychainItem.swift @@ -3,7 +3,7 @@ import Foundation // MARK: - PasskeyKeychainItem -/// The keychain items used by the SDK-backed passkey scenarios. +/// The keychain items used by the passkey scenarios. /// enum PasskeyKeychainItem: Equatable, KeychainItem { /// The keychain item for the synthetic identity used to bootstrap the SDK client, so the diff --git a/TestHarnessShared/Core/Autofill/Passkey/PasskeyService.swift b/TestHarnessShared/Core/Autofill/Passkey/PasskeyService.swift index 181aeb8eea..c4e25d048c 100644 --- a/TestHarnessShared/Core/Autofill/Passkey/PasskeyService.swift +++ b/TestHarnessShared/Core/Autofill/Passkey/PasskeyService.swift @@ -7,8 +7,7 @@ import Foundation // sourcery: AutoMockable /// A service that performs passkey registration and authentication directly through -/// `BitwardenSdk`'s Fido2 client — the same way the main Bitwarden app and its AutoFill -/// extension do — without going through the OS's passkey UI or a real vault. +/// `BitwardenSdk`'s Fido2 client. /// public protocol PasskeyService: AnyObject { /// Asserts a passkey for `rpId` against previously registered credentials. @@ -42,7 +41,7 @@ public protocol PasskeyService: AnyObject { // MARK: - HasPasskeyService -/// A protocol for an object that provides an `PasskeyService`. +/// A protocol for an object that provides a `PasskeyService`. protocol HasPasskeyService { /// The service used to perform passkey registration and authentication through the /// Bitwarden SDK. diff --git a/TestHarnessShared/Core/Autofill/Passkey/StoredCipher.swift b/TestHarnessShared/Core/Autofill/Passkey/StoredCipher.swift index 3dc7e00346..b13fbb8aca 100644 --- a/TestHarnessShared/Core/Autofill/Passkey/StoredCipher.swift +++ b/TestHarnessShared/Core/Autofill/Passkey/StoredCipher.swift @@ -3,7 +3,7 @@ import Foundation // MARK: - StoredCipher -/// A `Codable` mirror of the handful of `Cipher`/`Fido2Credential` fields the SDK-backed passkey +/// A `Codable` mirror of the handful of `Cipher`/`Fido2Credential` fields the passkey /// scenarios actually populate, since `BitwardenSdk.Cipher` doesn't itself conform to `Codable`. /// Every cipher these scenarios create is a login-type cipher with exactly one Fido2 credential, /// so the remaining `Cipher` fields are reconstructed with the same fixed defaults used when the diff --git a/TestHarnessShared/Core/Autofill/Passkey/SyntheticIdentity.swift b/TestHarnessShared/Core/Autofill/Passkey/SyntheticIdentity.swift index f04a09e843..b2feeabba1 100644 --- a/TestHarnessShared/Core/Autofill/Passkey/SyntheticIdentity.swift +++ b/TestHarnessShared/Core/Autofill/Passkey/SyntheticIdentity.swift @@ -2,10 +2,9 @@ import Foundation // MARK: - SyntheticIdentity -/// The synthetic, throwaway identity used to bootstrap the SDK-backed passkey scenarios' ephemeral +/// The synthetic, throwaway identity used to bootstrap the passkey scenarios' ephemeral /// `BitwardenSdk.Client`. Persisted in the keychain so the same crypto keys can be reconstructed -/// across app launches — without it, a freshly generated identity on the next launch couldn't -/// decrypt any previously-registered credential. +/// across app launches. /// struct SyntheticIdentity: Codable, Equatable { /// The synthetic account's email address. diff --git a/TestHarnessShared/UI/Autofill/Passkey/RegisterPasskeyAction.swift b/TestHarnessShared/UI/Autofill/Passkey/RegisterPasskeyAction.swift new file mode 100644 index 0000000000..7a72c1d530 --- /dev/null +++ b/TestHarnessShared/UI/Autofill/Passkey/RegisterPasskeyAction.swift @@ -0,0 +1,14 @@ +// MARK: - RegisterPasskeyAction + +/// Actions that can be processed by an `RegisterPasskeyProcessor`. +/// +enum RegisterPasskeyAction: Equatable { + /// The display name text field changed. + case displayNameChanged(String) + + /// The relying party ID text field changed. + case rpIdChanged(String) + + /// The username text field changed. + case userNameChanged(String) +} diff --git a/TestHarnessShared/UI/Autofill/Passkey/RegisterPasskeyEffect.swift b/TestHarnessShared/UI/Autofill/Passkey/RegisterPasskeyEffect.swift new file mode 100644 index 0000000000..dbd60604df --- /dev/null +++ b/TestHarnessShared/UI/Autofill/Passkey/RegisterPasskeyEffect.swift @@ -0,0 +1,8 @@ +// MARK: - RegisterPasskeyEffect + +/// Effects that can be processed by an `RegisterPasskeyProcessor`. +/// +enum RegisterPasskeyEffect: Equatable { + /// The user tapped the register passkey button. + case registerPasskey +} diff --git a/TestHarnessShared/UI/Autofill/Passkey/RegisterPasskeyProcessor.swift b/TestHarnessShared/UI/Autofill/Passkey/RegisterPasskeyProcessor.swift new file mode 100644 index 0000000000..72ba93062d --- /dev/null +++ b/TestHarnessShared/UI/Autofill/Passkey/RegisterPasskeyProcessor.swift @@ -0,0 +1,77 @@ +import BitwardenKit + +// MARK: - RegisterPasskeyProcessor + +/// The processor for the register passkey test screen. +/// +final class RegisterPasskeyProcessor: StateProcessor< + RegisterPasskeyState, + RegisterPasskeyAction, + RegisterPasskeyEffect, +> { + // MARK: Private Properties + + /// The coordinator that handles navigation. + private let coordinator: AnyCoordinator + + /// The service used to perform passkey registration through the Bitwarden SDK. + private let passkeyService: PasskeyService + + // MARK: Initialization + + /// Initializes an `RegisterPasskeyProcessor`. + /// + /// - Parameters: + /// - coordinator: The coordinator that handles navigation. + /// - passkeyService: The service used to perform passkey registration through the + /// Bitwarden SDK. + /// + init( + coordinator: AnyCoordinator, + passkeyService: PasskeyService, + ) { + self.coordinator = coordinator + self.passkeyService = passkeyService + super.init(state: RegisterPasskeyState()) + } + + // MARK: Methods + + override func perform(_ effect: RegisterPasskeyEffect) async { + switch effect { + case .registerPasskey: + await registerPasskey() + } + } + + override func receive(_ action: RegisterPasskeyAction) { + switch action { + case let .displayNameChanged(newValue): + state.displayName = newValue + state.status = .idle + case let .rpIdChanged(newValue): + state.rpId = newValue + state.status = .idle + case let .userNameChanged(newValue): + state.userName = newValue + state.status = .idle + } + } + + // MARK: Private + + /// Orchestrates state transitions and calls `passkeyService.registerPasskey`. + private func registerPasskey() async { + state.status = .inProgress + do { + let result = try await passkeyService.registerPasskey( + rpId: state.rpId, + userName: state.userName, + displayName: state.displayName, + ) + state.status = .success(credentialId: result.credentialId.base64EncodedString()) + } catch { + state.status = .failure(error.localizedDescription) + } + } +} diff --git a/TestHarnessShared/UI/Autofill/Passkey/RegisterPasskeyProcessorTests.swift b/TestHarnessShared/UI/Autofill/Passkey/RegisterPasskeyProcessorTests.swift new file mode 100644 index 0000000000..6b7777f831 --- /dev/null +++ b/TestHarnessShared/UI/Autofill/Passkey/RegisterPasskeyProcessorTests.swift @@ -0,0 +1,109 @@ +import BitwardenKit +import BitwardenKitMocks +import BitwardenSdk +import TestHelpers +import XCTest + +@testable import TestHarnessShared + +// MARK: - RegisterPasskeyProcessorTests + +/// Tests for `RegisterPasskeyProcessor`. +/// +class RegisterPasskeyProcessorTests: BitwardenTestCase { + // MARK: Properties + + var coordinator: MockCoordinator! + var passkeyService: MockPasskeyService! + var subject: RegisterPasskeyProcessor! + + // MARK: Setup & Teardown + + @MainActor + override func setUp() { + super.setUp() + coordinator = MockCoordinator() + passkeyService = MockPasskeyService() + subject = RegisterPasskeyProcessor( + coordinator: coordinator.asAnyCoordinator(), + passkeyService: passkeyService, + ) + } + + override func tearDown() { + super.tearDown() + coordinator = nil + passkeyService = nil + subject = nil + } + + // MARK: Action Tests + + /// `receive(.displayNameChanged)` updates the display name in state. + @MainActor + func test_receive_displayNameChanged() { + subject.receive(.displayNameChanged("Test User")) + XCTAssertEqual(subject.state.displayName, "Test User") + } + + /// `receive(.rpIdChanged)` updates the RP ID in state. + @MainActor + func test_receive_rpIdChanged() { + subject.receive(.rpIdChanged("bitwarden.com")) + XCTAssertEqual(subject.state.rpId, "bitwarden.com") + } + + /// `receive(.userNameChanged)` updates the username in state. + @MainActor + func test_receive_userNameChanged() { + subject.receive(.userNameChanged("testuser")) + XCTAssertEqual(subject.state.userName, "testuser") + } + + // MARK: Effect Tests + + /// `perform(.registerPasskey)` passes the current state values to the SDK passkey service. + @MainActor + func test_perform_registerPasskey_passesStateValues() async { + subject.receive(.rpIdChanged("example.com")) + subject.receive(.userNameChanged("alice")) + subject.receive(.displayNameChanged("Alice Smith")) + passkeyService.registerPasskeyReturnValue = .fixture() + + await subject.perform(.registerPasskey) + + XCTAssertEqual( + passkeyService.registerPasskeyReceivedArguments?.rpId, + "example.com", + ) + XCTAssertEqual( + passkeyService.registerPasskeyReceivedArguments?.userName, + "alice", + ) + XCTAssertEqual( + passkeyService.registerPasskeyReceivedArguments?.displayName, + "Alice Smith", + ) + } + + /// `perform(.registerPasskey)` sets status to `.failure` when registration throws. + @MainActor + func test_perform_registerPasskey_failure() async { + passkeyService.registerPasskeyThrowableError = BitwardenTestError.example + + await subject.perform(.registerPasskey) + + XCTAssertEqual(subject.state.status, .failure(BitwardenTestError.example.localizedDescription)) + } + + /// `perform(.registerPasskey)` sets status to `.success` with the resulting credential ID + /// when registration succeeds. + @MainActor + func test_perform_registerPasskey_success() async { + passkeyService.registerPasskeyReturnValue = .fixture(credentialId: Data([0x01, 0x02, 0x03])) + + await subject.perform(.registerPasskey) + + XCTAssertEqual(subject.state.status, .success(credentialId: Data([0x01, 0x02, 0x03]).base64EncodedString())) + } +} diff --git a/TestHarnessShared/UI/Autofill/Passkey/RegisterPasskeyState.swift b/TestHarnessShared/UI/Autofill/Passkey/RegisterPasskeyState.swift new file mode 100644 index 0000000000..0ea3daffdf --- /dev/null +++ b/TestHarnessShared/UI/Autofill/Passkey/RegisterPasskeyState.swift @@ -0,0 +1,41 @@ +import Foundation + +// MARK: - RegisterPasskeyState + +/// The state for the register passkey test screen. +/// +struct RegisterPasskeyState: Equatable { + // MARK: Types + + /// The current status of an passkey registration attempt. + enum RegistrationStatus: Equatable { + /// Registration failed with the associated error description. + case failure(String) + + /// No registration attempt has been made. + case idle + + /// A registration request is in progress. + case inProgress + + /// Registration completed successfully, producing the credential ID below. + case success(credentialId: String) + } + + // MARK: Properties + + /// The display name for the passkey credential. + var displayName: String = "" + + /// The relying party identifier (RP ID) for passkey registration. + var rpId: String = "bitwarden.pw" + + /// The current registration status. + var status: RegistrationStatus = .idle + + /// The title of the screen. + var title: String = Localizations.registerPasskey + + /// The username for the passkey credential. + var userName: String = "" +} diff --git a/TestHarnessShared/UI/Autofill/Passkey/RegisterPasskeyView.swift b/TestHarnessShared/UI/Autofill/Passkey/RegisterPasskeyView.swift new file mode 100644 index 0000000000..5fe683e60d --- /dev/null +++ b/TestHarnessShared/UI/Autofill/Passkey/RegisterPasskeyView.swift @@ -0,0 +1,154 @@ +import BitwardenKit +import SwiftUI + +// MARK: - RegisterPasskeyView + +/// A view that allows registering a passkey directly through the Bitwarden SDK, the same way +/// the main Bitwarden app and its AutoFill extension do. +/// +struct RegisterPasskeyView: View { + // MARK: Properties + + /// The store used to render the view. + @ObservedObject var store: Store + + // MARK: View + + var body: some View { + content + .navigationTitle(store.state.title) + .navigationBarTitleDisplayMode(.large) + } + + // MARK: Private Views + + private var content: some View { + Form { + credentialsSection + registerButtonSection + statusSection + } + } + + /// The section containing the relying party ID, username, and display name fields. + private var credentialsSection: some View { + Section { + TextField( + Localizations.relyingPartyId, + text: store.binding( + get: \.rpId, + send: RegisterPasskeyAction.rpIdChanged, + ), + ) + .accessibilityIdentifier("RelyingPartyIdEntry") + .textContentType(.URL) + .textInputAutocapitalization(.never) + .autocorrectionDisabled() + + TextField( + Localizations.username, + text: store.binding( + get: \.userName, + send: RegisterPasskeyAction.userNameChanged, + ), + ) + .accessibilityIdentifier("UsernameEntry") + .textContentType(.username) + .textInputAutocapitalization(.never) + .autocorrectionDisabled() + + TextField( + Localizations.displayName, + text: store.binding( + get: \.displayName, + send: RegisterPasskeyAction.displayNameChanged, + ), + ) + .accessibilityIdentifier("DisplayNameEntry") + .textContentType(.name) + } header: { + Text(Localizations.credentials) + } footer: { + Text(Localizations.registerPasskeyFormDescriptionLong) + } + } + + /// The section containing the button that triggers passkey registration. + private var registerButtonSection: some View { + Section { + Button { + Task { await store.perform(.registerPasskey) } + } label: { + HStack { + Text(Localizations.registerPasskey) + Spacer() + if store.state.status == .inProgress { + ProgressView() + } + } + } + .disabled(store.state.status == .inProgress || store.state.rpId.isEmpty || store.state.userName.isEmpty) + .accessibilityIdentifier("RegisterPasskeyButton") + } + } + + /// The section displaying the result of the most recent registration attempt, if any. + @ViewBuilder private var statusSection: some View { + switch store.state.status { + case .idle, .inProgress: + EmptyView() + case let .success(credentialId): + Section { + Label(Localizations.passkeyRegisteredSuccessfully, systemImage: "checkmark.circle.fill") + .foregroundStyle(.green) + .accessibilityIdentifier("RegistrationSuccessLabel") + Text(Localizations.xColonY(Localizations.credentialId, credentialId)) + .font(.footnote) + } header: { + Text(Localizations.registrationResult) + } + case let .failure(message): + Section { + Label(message, systemImage: "xmark.circle.fill") + .foregroundStyle(.red) + .accessibilityIdentifier("RegistrationFailureLabel") + } header: { + Text(Localizations.registrationResult) + } + } + } +} + +// MARK: - Previews + +#if DEBUG +#Preview("Idle") { + NavigationView { + RegisterPasskeyView(store: Store(processor: StateProcessor(state: RegisterPasskeyState()))) + } +} + +#Preview("Success") { + NavigationView { + RegisterPasskeyView( + store: Store(processor: StateProcessor(state: { + var state = RegisterPasskeyState() + state.status = .success(credentialId: "AQIDBA==") + return state + }())), + ) + } +} + +#Preview("Failure") { + NavigationView { + RegisterPasskeyView( + store: Store(processor: StateProcessor(state: { + var state = RegisterPasskeyState() + state.status = .failure("The SDK client could not be initialized.") + return state + }())), + ) + } +} +#endif diff --git a/TestHarnessShared/UI/Platform/Application/Support/Localizations/en.lproj/Localizable.strings b/TestHarnessShared/UI/Platform/Application/Support/Localizations/en.lproj/Localizable.strings index 2aee3c9c85..5091643ff7 100644 --- a/TestHarnessShared/UI/Platform/Application/Support/Localizations/en.lproj/Localizable.strings +++ b/TestHarnessShared/UI/Platform/Application/Support/Localizations/en.lproj/Localizable.strings @@ -11,7 +11,9 @@ "CreateAccountForm" = "Create Account Form"; "CreateAccountFormDescriptionLong" = "Fill in the form and tap Create Account. On supported devices, iOS will prompt to save the credential via the active password provider."; "CreatePasskey" = "Create Passkey"; +"CredentialId" = "Credential ID"; "Credentials" = "Credentials"; +"DisplayName" = "Display Name"; "EnterCardDetailsAbove" = "Enter card details above"; "EnterCredentialsAbove" = "Enter credentials above"; "EnterTOTPCodeAbove" = "Enter TOTP code above"; @@ -20,10 +22,14 @@ "FileShare" = "File Share"; "FormValues" = "Form Values"; "NoMatchingCredentialReceived" = "No stored credential matches this relying party ID."; -"PasskeyAutofill" = "Passkey Autofill"; -"Result" = "Result"; +"PasskeyRegisteredSuccessfully" = "Passkey registered successfully via the Bitwarden SDK."; "Password" = "Password"; "PasswordsDoNotMatch" = "Passwords do not match"; +"RegisterPasskey" = "Register Passkey"; +"RegisterPasskeyFormDescriptionLong" = "Fill in the fields above, then tap Register Passkey. The credential is created directly through the Bitwarden SDK, with no OS passkey sheet or separate Bitwarden app involved."; +"RegistrationResult" = "Registration Result"; +"RelyingPartyId" = "Relying Party ID"; +"Result" = "Result"; "SecurityCode" = "Security Code"; "ShareFile" = "Share File"; "SharesASamplePDFDescriptionLong" = "Shares a sample PDF containing the text \"Hello World\". Exercises the UTType.data → URL → file Send path in the share extension."; diff --git a/TestHarnessShared/UI/Platform/Root/RootCoordinator.swift b/TestHarnessShared/UI/Platform/Root/RootCoordinator.swift index 3c1a1f85ff..f64f6db646 100644 --- a/TestHarnessShared/UI/Platform/Root/RootCoordinator.swift +++ b/TestHarnessShared/UI/Platform/Root/RootCoordinator.swift @@ -46,6 +46,8 @@ class RootCoordinator: Coordinator, HasStackNavigator { showFileShare() case .scenarioPicker: showScenarioPicker() + case .registerPasskey: + showRegisterPasskey() case .simpleLoginForm: showSimpleLoginForm() case .totpAutofillForm: @@ -105,6 +107,18 @@ class RootCoordinator: Coordinator, HasStackNavigator { stackNavigator?.replace(view) } + /// Shows the register passkey test screen. + /// + private func showRegisterPasskey() { + let processor = RegisterPasskeyProcessor( + coordinator: asAnyCoordinator(), + passkeyService: services.passkeyService, + ) + let view = RegisterPasskeyView(store: Store(processor: processor)) + let viewController = UIHostingController(rootView: view) + stackNavigator?.push(viewController) + } + /// Shows the simple login form test screen. /// private func showSimpleLoginForm() { diff --git a/TestHarnessShared/UI/Platform/Root/RootRoute.swift b/TestHarnessShared/UI/Platform/Root/RootRoute.swift index 3eabbf2a75..a7f2133c97 100644 --- a/TestHarnessShared/UI/Platform/Root/RootRoute.swift +++ b/TestHarnessShared/UI/Platform/Root/RootRoute.swift @@ -18,6 +18,9 @@ public enum RootRoute { /// A route to the scenario picker home screen. case scenarioPicker + /// A route to the register passkey test screen. + case registerPasskey + /// A route to the simple login form test screen. case simpleLoginForm diff --git a/TestHarnessShared/UI/Platform/Root/ScenarioPicker/ScenarioPickerState.swift b/TestHarnessShared/UI/Platform/Root/ScenarioPicker/ScenarioPickerState.swift index ac5073624a..4f7d003158 100644 --- a/TestHarnessShared/UI/Platform/Root/ScenarioPicker/ScenarioPickerState.swift +++ b/TestHarnessShared/UI/Platform/Root/ScenarioPicker/ScenarioPickerState.swift @@ -33,12 +33,8 @@ struct ScenarioPickerState: Equatable { ScenarioItem(id: "createAccountForm", title: Localizations.createAccountForm, route: .createAccountForm), ScenarioItem(id: "simpleLoginForm", title: Localizations.simpleLoginForm, route: .simpleLoginForm), ScenarioItem(id: "totpAutofillForm", title: Localizations.totpAutofillForm, route: .totpAutofillForm), - ScenarioItem(id: "passkeyAutofill", title: Localizations.passkeyAutofill, route: nil), - ScenarioItem( - id: "dateFieldPicker", - title: Localizations.dateFieldPicker, - route: .dateFieldPickerShowcase, - ), + ScenarioItem(id: "dateFieldPicker", title: Localizations.dateFieldPicker, route: .dateFieldPickerShowcase), + ScenarioItem(id: "registerPasskey", title: Localizations.registerPasskey, route: .registerPasskey), ] if #available(iOS 17, *) { items.append( diff --git a/TestHarnessShared/UI/Platform/Root/ScenarioPicker/ScenarioPickerView.swift b/TestHarnessShared/UI/Platform/Root/ScenarioPicker/ScenarioPickerView.swift index 3c52602177..7f5cb38640 100644 --- a/TestHarnessShared/UI/Platform/Root/ScenarioPicker/ScenarioPickerView.swift +++ b/TestHarnessShared/UI/Platform/Root/ScenarioPicker/ScenarioPickerView.swift @@ -40,8 +40,8 @@ struct ScenarioPickerView: View { case Localizations.cardAutofillForm: "ScenarioButton_CardForm" case Localizations.createAccountForm: "ScenarioButton_CreateAccountForm" case Localizations.fileShare: "ScenarioButton_FileShare" - case Localizations.passkeyAutofill: "ScenarioButton_Passkey" case Localizations.simpleLoginForm: "ScenarioButton_LoginForm" + case Localizations.totpAutofillForm: "ScenarioButton_TOTPForm" default: "ScenarioButton_\(scenario.title)" } }())