Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 52 additions & 23 deletions iOCNotes/Store.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ final class Store: Logging, Storing {
///
var pollingTask: Task<Void, any Error>?

/// 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.
///
Expand Down Expand Up @@ -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)?

Expand All @@ -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
Expand All @@ -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
}

Expand Down
18 changes: 16 additions & 2 deletions iOCNotes/Views/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,15 +37,29 @@ 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()
}
.onAppear {
// 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) {
Expand Down
Loading