diff --git a/ElementX/Sources/Screens/Authentication/ServerSelectionScreen/ServerSelectionScreenViewModel.swift b/ElementX/Sources/Screens/Authentication/ServerSelectionScreen/ServerSelectionScreenViewModel.swift index 6f6d8c558..4529b61cf 100644 --- a/ElementX/Sources/Screens/Authentication/ServerSelectionScreen/ServerSelectionScreenViewModel.swift +++ b/ElementX/Sources/Screens/Authentication/ServerSelectionScreen/ServerSelectionScreenViewModel.swift @@ -20,6 +20,8 @@ class ServerSelectionScreenViewModel: ServerSelectionScreenViewModelType, Server /// Debounces autocompletion from user input private var debouncedAutocompleteTask: Task? + /// The confirmation in progress, used to ignore repeated confirms (e.g. Continue then keyboard Done). + private var confirmTask: Task? private var actionsSubject: PassthroughSubject = .init() @@ -65,11 +67,15 @@ class ServerSelectionScreenViewModel: ServerSelectionScreenViewModelType, Server guard state.textField != textField else { return } // stop the infinite loop state.textField = textField case .confirm: - switch state.mode { - case .userInput: - Task { await configureHomeserver() } - case .picker: - Task { await pickServer() } + guard confirmTask == nil else { return } + confirmTask = Task { + switch state.mode { + case .userInput: + await configureHomeserver() + case .picker: + await pickServer() + } + confirmTask = nil } case .clearFooterError: clearFooterError() diff --git a/UnitTests/Sources/ServerSelectionScreenViewModelTests.swift b/UnitTests/Sources/ServerSelectionScreenViewModelTests.swift index cbae0e1bd..51f408460 100644 --- a/UnitTests/Sources/ServerSelectionScreenViewModelTests.swift +++ b/UnitTests/Sources/ServerSelectionScreenViewModelTests.swift @@ -210,6 +210,36 @@ struct ServerSelectionScreenViewModelTests { #expect(clientFactory.makeAuthenticationClientServerNameOrBaseURLSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 2) } + @Test + mutating func userInputConfirmTwiceContinuesOnce() async throws { + // Given a view model for login with a server that doesn't support OAuth. + try setup(authenticationFlow: .login) + context.serverNameOrBaseURL = Self.passwordOnlyServer + + // When confirming a second time before the first confirmation has finished (e.g. Continue then keyboard Done). + var continueCount = 0 + let deferredFailure = deferFailure(viewModel.actions, timeout: .seconds(1), message: "The flow should only continue once.") { action in + guard action.isContinueWithPassword else { return false } + continueCount += 1 + return continueCount > 1 + } + context.send(viewAction: .confirm) + context.send(viewAction: .confirm) + try await deferredFailure.fulfill() + + // Then the server should only be configured once. + #expect(continueCount == 1) + #expect(clientFactory.makeAuthenticationClientServerNameOrBaseURLSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 1) + + // And when confirming again once finished (e.g. after navigating back from the login screen). + let deferred = deferFulfillment(viewModel.actions) { $0.isContinueWithPassword } + context.send(viewAction: .confirm) + try await deferred.fulfill() + + // Then the flow should continue again. + #expect(clientFactory.makeAuthenticationClientServerNameOrBaseURLSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 2) + } + // MARK: - Picker mode @Test