@@ -372,9 +372,8 @@ struct PWAWebView: UIViewRepresentable {
372372
373373 // MARK: - Coordinator
374374
375- class Coordinator : NSObject , WKNavigationDelegate , WKUIDelegate , WKScriptMessageHandler , UIScrollViewDelegate , ASWebAuthenticationPresentationContextProviding {
375+ class Coordinator : NSObject , WKNavigationDelegate , WKUIDelegate , WKScriptMessageHandler , UIScrollViewDelegate , ASWebAuthenticationPresentationContextProviding , SFSafariViewControllerDelegate {
376376 var parent : PWAWebView
377- var authSession : ASWebAuthenticationSession ?
378377
379378 init ( _ parent: PWAWebView ) {
380379 self . parent = parent
@@ -392,60 +391,89 @@ struct PWAWebView: UIViewRepresentable {
392391
393392 // MARK: - External Browser Authentication (for Google, etc.)
394393
395- /// Start authentication in Safari for providers that block WebView
396- /// We use SFSafariViewController which shares cookies with Safari
397- /// When auth completes, the callback URL will open the app via Universal Links
394+ var safariVC : SFSafariViewController ?
395+
396+ /// Start authentication using SFSafariViewController for providers that block WebView
397+ /// Google accepts SFSafariViewController as a "system browser" (not an embedded webview)
398+ /// Unlike ASWebAuthenticationSession, this works with the server's HTTPS callback URL
398399 func startExternalBrowserAuth( url: URL ) {
399- print ( " [AUTH] 🌐 Opening Safari for auth: \( url. absoluteString) " )
400- print ( " [AUTH] 💡 After sign-in, the app should reopen automatically via Universal Links " )
401- print ( " [AUTH] 💡 If not, user can return to app and pull-to-refresh " )
402-
403- // Open in Safari (external browser)
404- // Safari will handle the OAuth flow and cookies will be stored in Safari's cookie store
405- // When auth completes at terrier.scottylabs.org/auth/callback,
406- // Universal Links will open the app if configured
407- UIApplication . shared. open ( url, options: [ : ] ) { success in
408- if success {
409- print ( " [AUTH] ✅ Opened Safari successfully " )
400+ print ( " [AUTH] 🌐 Opening SFSafariViewController for: \( url. absoluteString) " )
401+
402+ let config = SFSafariViewController . Configuration ( )
403+ config. entersReaderIfAvailable = false
404+ config. barCollapsingEnabled = true
405+
406+ let safari = SFSafariViewController ( url: url, configuration: config)
407+ safari. delegate = self
408+ safari. preferredControlTintColor = . systemBlue
409+ safari. dismissButtonStyle = . cancel
410+
411+ safariVC = safari
412+
413+ // Present the Safari view controller
414+ DispatchQueue . main. async {
415+ if let scene = UIApplication . shared. connectedScenes. first as? UIWindowScene ,
416+ let rootVC = scene. windows. first? . rootViewController {
417+ // Find the topmost presented view controller
418+ var topVC = rootVC
419+ while let presented = topVC. presentedViewController {
420+ topVC = presented
421+ }
422+ topVC. present ( safari, animated: true )
423+ print ( " [AUTH] ✅ SFSafariViewController presented " )
410424 } else {
411- print ( " [AUTH] ❌ Failed to open Safari " )
425+ print ( " [AUTH] ❌ Could not find root view controller " )
412426 }
413427 }
414428 }
415429
416- /// Handle the callback URL from external auth
417- private func handleAuthCallback( _ callbackURL: URL ) {
418- // The callback URL will be like: terrier://auth/callback?code=xxx&state=yyy
419- // We need to convert it to: https://terrier.scottylabs.org/auth/callback?code=xxx&state=yyy
420-
421- var pathComponents = [ String] ( )
422- if let host = callbackURL. host {
423- pathComponents. append ( host)
424- }
425- if !callbackURL. path. isEmpty && callbackURL. path != " / " {
426- pathComponents. append ( String ( callbackURL. path. dropFirst ( ) ) )
427- }
428- let path = pathComponents. joined ( separator: " / " )
430+ /// Called when Safari view finishes loading - check if we've returned to the app domain after auth
431+ func safariViewController( _ controller: SFSafariViewController , didCompleteInitialLoad didLoadSuccessfully: Bool ) {
432+ print ( " [AUTH] 📄 Safari initial load complete, success: \( didLoadSuccessfully) " )
433+ }
434+
435+ /// Called when the user taps Done button or Safari redirects
436+ func safariViewControllerDidFinish( _ controller: SFSafariViewController ) {
437+ print ( " [AUTH] ⏹️ User dismissed Safari view controller " )
438+ safariVC = nil
429439
430- var webURLString = " https://terrier.scottylabs.org/ \( path) "
431- if let query = callbackURL. query {
432- webURLString += " ? \( query) "
440+ // Sync cookies and reload the WebView to pick up any session changes
441+ print ( " [AUTH] 🍪 Syncing cookies after Safari dismiss... " )
442+ syncCookiesToWebView { [ weak self] in
443+ DispatchQueue . main. async {
444+ self ? . parent. state. webView? . reload ( )
445+ }
433446 }
447+ }
448+
449+ /// Called when Safari redirects to a URL - detect auth completion
450+ func safariViewController( _ controller: SFSafariViewController , initialLoadDidRedirectTo URL: URL ) {
451+ print ( " [AUTH] 🔀 Safari redirected to: \( URL . absoluteString) " )
434452
435- // Sync cookies from the shared HTTP cookie storage to WKWebView
436- // ASWebAuthenticationSession stores cookies in HTTPCookieStorage.shared
437- print ( " [AUTH] 🍪 Syncing cookies from system store to WKWebView... " )
438- self . syncCookiesToWebView {
439- if let webURL = URL ( string: webURLString) {
440- print ( " [AUTH] 🔄 Loading callback in WebView: \( webURL. absoluteString) " )
441- DispatchQueue . main. async {
442- self . parent. state. webView? . load ( URLRequest ( url: webURL) )
453+ // Check if we've returned to our app's domain after auth callback
454+ if let host = URL . host? . lowercased ( ) ,
455+ host. contains ( " scottylabs.org " ) ,
456+ !URL. path. contains ( " /realms/ " ) { // Not still in Keycloak login
457+
458+ // Auth likely completed - dismiss Safari and sync session
459+ print ( " [AUTH] ✅ Detected return to app domain, dismissing Safari... " )
460+
461+ DispatchQueue . main. asyncAfter ( deadline: . now( ) + 0.5 ) { [ weak self] in
462+ controller. dismiss ( animated: true ) {
463+ self ? . safariVC = nil
464+ print ( " [AUTH] 🍪 Syncing cookies after auth completion... " )
465+ self ? . syncCookiesToWebView {
466+ DispatchQueue . main. async {
467+ // Reload to the current URL (which is the auth callback result)
468+ self ? . parent. state. webView? . load ( URLRequest ( url: URL) )
469+ }
470+ }
443471 }
444472 }
445473 }
446474 }
447475
448- /// Sync cookies from HTTPCookieStorage.shared to WKWebView 's cookie store
476+ /// Sync cookies from Safari 's shared cookie storage to WKWebView
449477 private func syncCookiesToWebView( completion: @escaping ( ) -> Void ) {
450478 let sharedCookies = HTTPCookieStorage . shared. cookies ?? [ ]
451479 let wkCookieStore = WKWebsiteDataStore . default ( ) . httpCookieStore
0 commit comments