From 3f3ca50e36acdc8013783f69a4a72956fbcac9c0 Mon Sep 17 00:00:00 2001 From: Julius Knorr Date: Mon, 13 Jul 2026 12:44:38 +0200 Subject: [PATCH] fix: report login flow completion and errors Signed-off-by: Julius Knorr Assisted-by: Codex:GPT-5 --- iOCNotes/Store.swift | 75 ++++++++++++++++++++++---------- iOCNotes/Views/ContentView.swift | 18 +++++++- 2 files changed, 68 insertions(+), 25 deletions(-) diff --git a/iOCNotes/Store.swift b/iOCNotes/Store.swift index d4851a45..a02a5371 100644 --- a/iOCNotes/Store.swift +++ b/iOCNotes/Store.swift @@ -19,6 +19,9 @@ final class Store: Logging, Storing { /// var pollingTask: Task? + /// A terminal error reported by the active login flow. + var loginErrorMessage: String? + /// /// This singleton is necessary to conveniently expose the same environment object used in SwiftUI to the already existing UIKit code. /// @@ -289,31 +292,45 @@ final class Store: Logging, Storing { } } - private func getResponse(endpoint: URL, token: String, options: NKRequestOptions) async -> (url: String, user: String, appPassword: String)? { + private func getResponse(endpoint: URL, token: String, options: NKRequestOptions) async throws -> (url: String, user: String, appPassword: String)? { logger.debug("Getting login flow status...") - return await withCheckedContinuation { continuation in - NextcloudKit.shared.getLoginFlowV2Poll(token: token, endpoint: endpoint.absoluteString, options: options) { [self] server, loginName, appPassword, _, error in + return try await withCheckedThrowingContinuation { continuation in + NextcloudKit.shared.getLoginFlowV2Poll(token: token, endpoint: endpoint.absoluteString, options: options) { [self] server, loginName, appPassword, response, error in if error == .success, let urlBase = server, let user = loginName, let appPassword { logger.debug("Successfully got login flow status (server: \(urlBase), user: \(user), password: \(appPassword)).") continuation.resume(returning: (urlBase, user, appPassword)) - } else { - logger.debug("Failed to get login flow status.") + } else if response?.response?.statusCode == 404 { + logger.debug("Login flow is pending.") continuation.resume(returning: nil) + } else { + logger.error("Failed to get login flow status: \(error.localizedDescription, privacy: .public)") + continuation.resume(throwing: error.error) } } } } - func beginPolling(at url: URL) async throws -> URL { + private func getLoginFlow(for url: URL) async throws -> (endpoint: URL, login: URL, token: String) { + let options = NKRequestOptions(customUserAgent: userAgent) + + do { + return try await NextcloudKit.shared.getLoginFlowV2(serverUrl: url.absoluteString, options: options) + } catch { + logger.error("Failed to start login flow: \(error.localizedDescription, privacy: .public)") + throw error + } + } + + func beginPolling(at url: URL, dismiss: @MainActor @Sendable @escaping () -> Void) async throws -> URL { logger.debug("Beginning polling at \(url.absoluteString)") + loginErrorMessage = nil let (_, serverInfoResult) = await NextcloudKit.shared.getServerStatusAsync(serverUrl: url.absoluteString) switch serverInfoResult { case .success: - let loginOptions = NKRequestOptions(customUserAgent: userAgent) - let (endpoint, loginAddress, token) = try await NextcloudKit.shared.getLoginFlowV2(serverUrl: url.absoluteString, options: loginOptions) + let (endpoint, loginAddress, token) = try await getLoginFlow(for: url) let options = NKRequestOptions(customUserAgent: userAgent) var grantValues: (url: String, user: String, appPassword: String)? @@ -330,21 +347,33 @@ final class Store: Logging, Storing { self.pollingTask = nil } - repeat { - try Task.checkCancellation() - grantValues = await getResponse(endpoint: endpoint, token: token, options: options) - try await Task.sleep(for: .seconds(1)) - } while grantValues == nil - - guard let grantValues else { - return + do { + repeat { + try Task.checkCancellation() + grantValues = try await getResponse(endpoint: endpoint, token: token, options: options) + + if grantValues == nil { + try await Task.sleep(for: .seconds(1)) + } + } while grantValues == nil + + guard let grantValues else { + return + } + + guard let host = URL(string: grantValues.url) else { + throw NKError.urlError.error + } + + addAccount(host: host, name: grantValues.user, password: grantValues.appPassword) + dismiss() + } catch is CancellationError { + logger.debug("Login flow polling was cancelled.") + } catch { + logger.error("Login flow polling failed: \(error.localizedDescription, privacy: .public)") + loginErrorMessage = error.localizedDescription + dismiss() } - - guard let host = URL(string: grantValues.url) else { - return - } - - addAccount(host: host, name: grantValues.user, password: grantValues.appPassword) } return loginAddress @@ -358,7 +387,7 @@ final class Store: Logging, Storing { logger.debug("Cancelling polling task.") guard let pollingTask else { - logger.error("Attempt to cancel polling task but no task is active!") + logger.debug("No active polling task to cancel.") return } diff --git a/iOCNotes/Views/ContentView.swift b/iOCNotes/Views/ContentView.swift index 24396ca3..11e2753c 100644 --- a/iOCNotes/Views/ContentView.swift +++ b/iOCNotes/Views/ContentView.swift @@ -37,8 +37,8 @@ struct ContentView: View { if store.accounts.isEmpty { ServerAddressView(backgroundColor: .constant(Color.accent), brandImage: Image("BrandLogo"), sharedAccounts: sharedAccounts, userAgent: userAgent) { host, name, password in store.addAccount(host: host, name: name, password: password) - } beginPolling: { url, _ in - try await store.beginPolling(at: url) + } beginPolling: { url, dismiss in + try await store.beginPolling(at: url, dismiss: dismiss) } cancelPolling: { store.cancelPolling() } @@ -46,6 +46,20 @@ struct ContentView: View { // The store must update its list of shared accounts when the login user interface is about to appear. store.readSharedAccounts() } + .alert("Login Failed", isPresented: Binding( + get: { store.loginErrorMessage != nil }, + set: { isPresented in + if isPresented == false { + store.loginErrorMessage = nil + } + } + )) { + Button("OK", role: .cancel) { + store.loginErrorMessage = nil + } + } message: { + Text(store.loginErrorMessage ?? "Unknown login error.") + } } else { TabView(selection: $selection) {