Skip to content
This repository was archived by the owner on Jun 18, 2026. It is now read-only.

Commit 34d8f9b

Browse files
committed
fix(iOS): use ASWebAuthenticationSession instead of WKWebView for login
WKWebView silently fails to complete cross-domain OIDC redirects (e.g. when Nextcloud delegates authentication to an external IdP like Authentik). The user authenticates successfully on the IdP side, but WKWebView drops the callback redirect back to the Nextcloud origin, leaving the login flow stuck in a polling loop that never resolves. Replace WKWebView with ASWebAuthenticationSession on iOS via a new LoginSheet view modifier that encapsulates the platform difference: - iOS: ASWebAuthenticationSession (system browser, handles OIDC/passkeys) - macOS: WKWebView sheet (unchanged behavior) ServerAddressView is now platform-agnostic — it just sets isPresented and the modifier does the right thing per platform. Credentials continue to be obtained via the host app's existing polling mechanism. Ref: nextcloud/ios#3996 (same fix applied to the main iOS app) Signed-off-by: Thomas Dhooghe <61279337+tdhooghe@users.noreply.github.com> Made-with: Cursor
1 parent 0b3cbdc commit 34d8f9b

2 files changed

Lines changed: 115 additions & 4 deletions

File tree

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
// SPDX-FileCopyrightText: Nextcloud GmbH
2+
// SPDX-FileCopyrightText: 2026 tdhooghe
3+
// SPDX-License-Identifier: GPL-3.0-or-later
4+
5+
import SwiftUI
6+
7+
#if os(iOS)
8+
import AuthenticationServices
9+
#endif
10+
11+
///
12+
/// Presents the Nextcloud Login Flow v2 authentication UI.
13+
///
14+
/// On **iOS**, this uses `ASWebAuthenticationSession` which opens a system browser sheet
15+
/// that properly handles cross-domain OIDC redirects, passkeys, and deep links.
16+
///
17+
/// On **macOS**, this presents a ``WebView`` in a sheet.
18+
///
19+
/// Credentials are obtained via the host app's polling mechanism on both platforms.
20+
///
21+
struct LoginSheet: ViewModifier {
22+
let userAgent: String?
23+
let onDismiss: () -> Void
24+
25+
@Binding var loginURL: URL?
26+
@Binding var isPresented: Bool
27+
28+
#if os(iOS)
29+
@State private var authSession: ASWebAuthenticationSession?
30+
@State private var sessionCoordinator = SessionCoordinator()
31+
#endif
32+
33+
init(loginURL: Binding<URL?>, isPresented: Binding<Bool>, userAgent: String?, onDismiss: @escaping () -> Void) {
34+
self._loginURL = loginURL
35+
self._isPresented = isPresented
36+
self.userAgent = userAgent
37+
self.onDismiss = onDismiss
38+
}
39+
40+
func body(content: Content) -> some View {
41+
content
42+
#if os(macOS)
43+
.sheet(isPresented: $isPresented, onDismiss: onDismiss) {
44+
WebView(initialURL: $loginURL, userAgent: userAgent)
45+
.ignoresSafeArea()
46+
.frame(minWidth: 800, minHeight: 800)
47+
}
48+
#else
49+
.onChange(of: isPresented) { _, presented in
50+
if presented, let url = loginURL {
51+
startAuthSession(url: url)
52+
} else {
53+
authSession?.cancel()
54+
authSession = nil
55+
}
56+
}
57+
#endif
58+
}
59+
60+
#if os(iOS)
61+
62+
private func startAuthSession(url: URL) {
63+
let session = ASWebAuthenticationSession(url: url, callbackURLScheme: "nc") { _, error in
64+
authSession = nil
65+
if let error = error as? ASWebAuthenticationSessionError, error.code == .canceledLogin {
66+
onDismiss()
67+
}
68+
}
69+
70+
session.presentationContextProvider = sessionCoordinator
71+
session.prefersEphemeralWebBrowserSession = true
72+
authSession = session
73+
session.start()
74+
}
75+
76+
#endif
77+
}
78+
79+
extension View {
80+
///
81+
/// Present the login authentication UI appropriate for the current platform.
82+
///
83+
/// See ``LoginSheet`` for the implementation.
84+
///
85+
func loginSheet(loginURL: Binding<URL?>, isPresented: Binding<Bool>, userAgent: String?, onDismiss: @escaping () -> Void) -> some View {
86+
modifier(LoginSheet(loginURL: loginURL, isPresented: isPresented, userAgent: userAgent, onDismiss: onDismiss))
87+
}
88+
}
89+
90+
// MARK: - ASWebAuthenticationSession Coordinator
91+
92+
#if os(iOS)
93+
94+
///
95+
/// Provides the presentation anchor for `ASWebAuthenticationSession` in SwiftUI contexts.
96+
///
97+
@MainActor
98+
private class SessionCoordinator: NSObject, ASWebAuthenticationPresentationContextProviding {
99+
nonisolated func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
100+
return MainActor.assumeIsolated {
101+
guard let windowScene = UIApplication.shared.connectedScenes.first(where: { $0.activationState == .foregroundActive }) as? UIWindowScene,
102+
let window = windowScene.windows.first(where: \.isKeyWindow)
103+
else {
104+
return ASPresentationAnchor()
105+
}
106+
return window
107+
}
108+
}
109+
}
110+
111+
#endif

Sources/SwiftNextcloudUI/Views/ServerAddressView.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,7 @@ public struct ServerAddressView: View, QRCodeParsing, URLSanitizing {
216216
.safeAreaPadding(.all)
217217
}
218218
.ignoresSafeArea()
219-
.webSheet(initialURL: $loginAddress, isPresented: $isPresentingWebView, userAgent: userAgent, onDismiss: endWebView)
219+
.loginSheet(loginURL: $loginAddress, isPresented: $isPresentingWebView, userAgent: userAgent, onDismiss: endWebView)
220220
.alert(String(localized: "Login Failed", comment: "Alert title"), isPresented: $isPresentingAlert) {
221221
Button(role: .cancel) {
222222
errorMessage = nil
@@ -294,12 +294,12 @@ public struct ServerAddressView: View, QRCodeParsing, URLSanitizing {
294294
}
295295

296296
///
297-
/// Dismisses the sheet with the web view.
297+
/// Dismisses the login UI.
298298
///
299299
/// Multiple paths can lead to here. In example:
300300
///
301-
/// - The user dismisses the sheet without logging in.
302-
/// - The login completed successfully.
301+
/// - The user dismisses the session without logging in.
302+
/// - The login completed successfully (detected by polling).
303303
///
304304
func endWebView() {
305305
if let pollingToken {

0 commit comments

Comments
 (0)