diff --git a/Sources/NextcloudKitUI/Modifiers/WebViewSheet.swift b/Sources/NextcloudKitUI/Modifiers/WebViewSheet.swift deleted file mode 100644 index 3ff02a8c..00000000 --- a/Sources/NextcloudKitUI/Modifiers/WebViewSheet.swift +++ /dev/null @@ -1,59 +0,0 @@ -// SPDX-FileCopyrightText: Nextcloud GmbH -// SPDX-FileCopyrightText: 2025 Iva Horn -// SPDX-License-Identifier: GPL-3.0-or-later - -import SwiftUI - -/// -/// The sheet wrapping ``WebView``. -/// -struct WebSheet: ViewModifier { - let onDismiss: () -> Void - let userAgent: String? - - @Binding var initialURL: URL? - @Binding var isPresented: Bool - - init(initialURL: Binding, isPresented: Binding, userAgent: String?, onDismiss: @escaping () -> Void) { - self._initialURL = initialURL - self._isPresented = isPresented - self.onDismiss = onDismiss - self.userAgent = userAgent - } - - func body(content: Content) -> some View { - #if os(iOS) - content.fullScreenCover(isPresented: $isPresented, onDismiss: onDismiss) { - WebView(initialURL: $initialURL, userAgent: userAgent) - .ignoresSafeArea() - } - #else - content.sheet(isPresented: $isPresented, onDismiss: onDismiss) { - WebView(initialURL: $initialURL, userAgent: userAgent) - .ignoresSafeArea() - .frame(minWidth: 800, minHeight: 800) - } - #endif - } -} - -extension View { - /// - /// Present a ``WebView`` full screen (a sheet on macOS, where full screen covers are unavailable). - /// - /// See ``WebSheet`` for the implementation. - /// - func webSheet(initialURL: Binding, isPresented: Binding, userAgent: String?, onDismiss: @escaping () -> Void) -> some View { - modifier(WebSheet(initialURL: initialURL, isPresented: isPresented, userAgent: userAgent, onDismiss: onDismiss)) - } -} - -#Preview { - ZStack { - Color.blue - .ignoresSafeArea() - } - .webSheet(initialURL: .constant(URL(string: "http://localhost:8080")), isPresented: .constant(true), userAgent: nil) { - print("Web sheet dismissed!") - } -} diff --git a/Sources/NextcloudKitUI/Views/Login/LoginFlowModel.swift b/Sources/NextcloudKitUI/Views/Login/LoginFlowModel.swift index ab016948..8446a66e 100644 --- a/Sources/NextcloudKitUI/Views/Login/LoginFlowModel.swift +++ b/Sources/NextcloudKitUI/Views/Login/LoginFlowModel.swift @@ -2,6 +2,7 @@ // SPDX-FileCopyrightText: 2026 Milen Pivchev // SPDX-License-Identifier: GPL-3.0-or-later +import AuthenticationServices #if os(iOS) import CodeScanner #endif @@ -40,8 +41,8 @@ final class LoginFlowModel: QRCodeParsing, URLSanitizing { /// State toggle for presenting the error alert. var isPresentingAlert = false - /// State toggle for presenting the web view. - var isPresentingWebView = false + /// State toggle for presenting the login page in the system browser. + var isPresentingLoginPage = false /// The login address acquired from the server through the login flow API. var loginAddress: URL? @@ -87,7 +88,7 @@ final class LoginFlowModel: QRCodeParsing, URLSanitizing { startPolling(token: token, endpoint: endpoint) loginAddress = login - isPresentingWebView = true + isPresentingLoginPage = true } catch { endLogin(error) } @@ -124,21 +125,36 @@ final class LoginFlowModel: QRCodeParsing, URLSanitizing { } } + func presentLoginPage(_ url: URL, using session: WebAuthenticationSession) async { + do { + _ = try await session.authenticate(using: url, callbackURLScheme: "nc", preferredBrowserSession: .ephemeral) + } catch { + // Already handled when the login completed or was cancelled. + guard isPresentingLoginPage else { return } + + if let error = error as? ASWebAuthenticationSessionError, error.code == .canceledLogin { + cancel() + } else { + endLogin(error) + } + } + } + private func complete(host: URL, name: String, password: String) { pollingTask = nil isActive = false - isPresentingWebView = false + isPresentingLoginPage = false addAccount(host, name, password) } /// - /// Stop polling. Called when the web view is dismissed, regardless of the reason. + /// Stop polling. Called when the login page is closed, regardless of the reason. /// func cancel() { pollingTask?.cancel() pollingTask = nil isActive = false - isPresentingWebView = false + isPresentingLoginPage = false } private func endLogin(_ error: Error?) { diff --git a/Sources/NextcloudKitUI/Views/Login/ServerAddressView.swift b/Sources/NextcloudKitUI/Views/Login/ServerAddressView.swift index bdeb6d9e..8c98a8f6 100644 --- a/Sources/NextcloudKitUI/Views/Login/ServerAddressView.swift +++ b/Sources/NextcloudKitUI/Views/Login/ServerAddressView.swift @@ -2,6 +2,7 @@ // SPDX-FileCopyrightText: 2025 Iva Horn // SPDX-License-Identifier: GPL-3.0-or-later +import AuthenticationServices import SwiftUI /// @@ -18,7 +19,6 @@ public typealias AddAccountHandler = (_ host: URL, _ name: String, _ password: S public struct ServerAddressView: View { let brandImage: Image var sharedAccounts: [SharedAccount] - let userAgent: String? /// /// Create a new server address view. @@ -27,14 +27,13 @@ public struct ServerAddressView: View { /// - backgroundColor: The main theme color the view should use. Foreground color will be adapted automatically based on this. /// - brandImage: The image to display on top of the server address view. Falls back to an SF Symbol placeholder in case of `nil`. /// - sharedAccounts: Any shared accounts from the app group being available for selection and faster login. - /// - userAgent: An optional user agent string to override the one used by ``WKWebView``. + /// - userAgent: An optional user agent for server requests. /// - addAccount: see ``AddAccountHandler``. /// public init(backgroundColor: Binding, brandImage: Image, sharedAccounts: [SharedAccount], userAgent: String? = nil, addAccount: @escaping AddAccountHandler) { self._backgroundColor = backgroundColor self.brandImage = brandImage self.sharedAccounts = sharedAccounts - self.userAgent = userAgent self._model = State(initialValue: LoginFlowModel(userAgent: userAgent, addAccount: addAccount)) } @@ -42,6 +41,7 @@ public struct ServerAddressView: View { @Environment(\.horizontalSizeClass) var horizontalSizeClass @Environment(\.verticalSizeClass) var verticalSizeClass + @Environment(\.webAuthenticationSession) private var webAuthenticationSession // MARK: - Bindings @@ -161,7 +161,12 @@ public struct ServerAddressView: View { .safeAreaPadding(.all) } .ignoresSafeArea() - .webSheet(initialURL: $model.loginAddress, isPresented: $model.isPresentingWebView, userAgent: userAgent, onDismiss: { model.cancel() }) + .task(id: model.isPresentingLoginPage) { + // Clearing the flag cancels this task, which closes the browser sheet. + guard model.isPresentingLoginPage, let loginAddress = model.loginAddress else { return } + + await model.presentLoginPage(loginAddress, using: webAuthenticationSession) + } .alert(String(localized: "Login Failed", bundle: .module, comment: "Alert title"), isPresented: $model.isPresentingAlert) { Button(role: .cancel) { model.errorMessage = nil diff --git a/Sources/NextcloudKitUI/Views/Login/WebView.swift b/Sources/NextcloudKitUI/Views/Login/WebView.swift index 4f3f4e5f..d1edb6f7 100644 --- a/Sources/NextcloudKitUI/Views/Login/WebView.swift +++ b/Sources/NextcloudKitUI/Views/Login/WebView.swift @@ -16,17 +16,13 @@ import WebKit /// All of the web view session data is not persistent. /// public struct WebView: ViewRepresentable { - /// - /// This must be a binding to support presentation inside sheets. - /// The way this view gets created and updated inside a sheet has a race condition with sheet presentation. - /// A sheet might be displayed before a passed in state variable might have been set. - /// - @Binding var initialURL: URL? + /// Loaded once when the web view is created, so SwiftUI updates don't reload the page. + let initialURL: URL? let userAgent: String? - public init(initialURL: Binding, userAgent: String? = nil) { - self._initialURL = initialURL + public init(initialURL: URL?, userAgent: String? = nil) { + self.initialURL = initialURL self.userAgent = userAgent } @@ -41,17 +37,11 @@ public struct WebView: ViewRepresentable { webView.customUserAgent = userAgent } - return webView - } - - func updateView(_ webView: WKWebView, context: Context) { - - guard let initialURL = initialURL else { - return + if let initialURL { + webView.load(URLRequest(url: initialURL)) } - let request = URLRequest(url: initialURL) - webView.load(request) + return webView } // MARK: - macOS @@ -62,9 +52,7 @@ public struct WebView: ViewRepresentable { makeView() } - public func updateNSView(_ webView: WKWebView, context: Context) { - updateView(webView, context: context) - } + public func updateNSView(_ webView: WKWebView, context: Context) {} // MARK: - iOS @@ -74,14 +62,12 @@ public struct WebView: ViewRepresentable { makeView() } - public func updateUIView(_ webView: WKWebView, context: Context) { - updateView(webView, context: context) - } + public func updateUIView(_ webView: WKWebView, context: Context) {} #endif } #Preview { // swiftlint:disable force_unwrapping - WebView(initialURL: .constant(URL(string: "http://localhost:8080")!), userAgent: nil) + WebView(initialURL: URL(string: "http://localhost:8080")!, userAgent: nil) }