瀏覽代碼

Route app links and Google sign-in through the in-app browser.

Made-with: Cursor
huzaifahayat12 1 周之前
父節點
當前提交
2bd2c3ec0e
共有 2 個文件被更改,包括 142 次插入14 次删除
  1. 112 1
      classroom_app/Auth/GoogleOAuthService.swift
  2. 30 13
      classroom_app/ViewController.swift

+ 112 - 1
classroom_app/Auth/GoogleOAuthService.swift

@@ -2,6 +2,7 @@ import Foundation
2 2
 import CryptoKit
3 3
 import AppKit
4 4
 import Network
5
+import WebKit
5 6
 
6 7
 struct GoogleOAuthTokens: Codable, Equatable {
7 8
     var accessToken: String
@@ -30,6 +31,7 @@ enum GoogleOAuthError: Error {
30 31
 
31 32
 final class GoogleOAuthService: NSObject {
32 33
     static let shared = GoogleOAuthService()
34
+    private var inAppOAuthWindowController: InAppOAuthWindowController?
33 35
 
34 36
     // Stored in UserDefaults so you can configure without rebuilding.
35 37
     // Put your OAuth Desktop client ID here (from Google Cloud Console).
@@ -152,7 +154,12 @@ final class GoogleOAuthService: NSObject {
152 154
         ]
153 155
 
154 156
         guard let authURL = components.url else { throw GoogleOAuthError.invalidCallbackURL }
155
-        guard NSWorkspace.shared.open(authURL) else { throw GoogleOAuthError.unableToOpenBrowser }
157
+        guard await openAuthURLInApp(authURL) else { throw GoogleOAuthError.unableToOpenBrowser }
158
+        defer {
159
+            Task { @MainActor [weak self] in
160
+                self?.closeInAppOAuthWindow()
161
+            }
162
+        }
156 163
         let callbackURL = try await loopback.waitForCallback()
157 164
 
158 165
         guard let returnedState = URLComponents(url: callbackURL, resolvingAgainstBaseURL: false)?
@@ -176,6 +183,29 @@ final class GoogleOAuthService: NSObject {
176 183
         )
177 184
     }
178 185
 
186
+    @MainActor
187
+    private func openAuthURLInApp(_ url: URL) -> Bool {
188
+        let controller: InAppOAuthWindowController
189
+        if let existing = inAppOAuthWindowController {
190
+            controller = existing
191
+        } else {
192
+            controller = InAppOAuthWindowController()
193
+            inAppOAuthWindowController = controller
194
+        }
195
+        controller.load(url: url)
196
+        controller.showWindow(nil)
197
+        controller.window?.makeKeyAndOrderFront(nil)
198
+        controller.window?.orderFrontRegardless()
199
+        NSApp.activate(ignoringOtherApps: true)
200
+        return true
201
+    }
202
+
203
+    @MainActor
204
+    private func closeInAppOAuthWindow() {
205
+        inAppOAuthWindowController?.close()
206
+        inAppOAuthWindowController = nil
207
+    }
208
+
179 209
     private func exchangeCodeForTokens(code: String, codeVerifier: String, redirectURI: String, clientId: String, clientSecret: String) async throws -> GoogleOAuthTokens {
180 210
         var request = URLRequest(url: URL(string: "https://oauth2.googleapis.com/token")!)
181 211
         request.httpMethod = "POST"
@@ -459,3 +489,84 @@ extension GoogleOAuthError: LocalizedError {
459 489
     }
460 490
 }
461 491
 
492
+@MainActor
493
+private final class InAppOAuthWindowController: NSWindowController, WKNavigationDelegate, WKUIDelegate {
494
+    private let webView: WKWebView
495
+
496
+    init() {
497
+        let config = WKWebViewConfiguration()
498
+        if #available(macOS 11.0, *) {
499
+            config.defaultWebpagePreferences.allowsContentJavaScript = true
500
+        }
501
+        self.webView = WKWebView(frame: .zero, configuration: config)
502
+        webView.translatesAutoresizingMaskIntoConstraints = false
503
+
504
+        let container = NSView()
505
+        container.translatesAutoresizingMaskIntoConstraints = false
506
+        container.addSubview(webView)
507
+        NSLayoutConstraint.activate([
508
+            webView.leadingAnchor.constraint(equalTo: container.leadingAnchor),
509
+            webView.trailingAnchor.constraint(equalTo: container.trailingAnchor),
510
+            webView.topAnchor.constraint(equalTo: container.topAnchor),
511
+            webView.bottomAnchor.constraint(equalTo: container.bottomAnchor)
512
+        ])
513
+
514
+        let window = NSWindow(
515
+            contentRect: NSRect(x: 0, y: 0, width: 980, height: 760),
516
+            styleMask: [.titled, .closable, .miniaturizable, .resizable],
517
+            backing: .buffered,
518
+            defer: false
519
+        )
520
+        window.title = "Google Sign-In"
521
+        window.contentView = container
522
+        window.center()
523
+        super.init(window: window)
524
+        webView.navigationDelegate = self
525
+        webView.uiDelegate = self
526
+    }
527
+
528
+    @available(*, unavailable)
529
+    required init?(coder: NSCoder) {
530
+        nil
531
+    }
532
+
533
+    func load(url: URL) {
534
+        webView.load(URLRequest(url: url))
535
+    }
536
+
537
+    func webView(
538
+        _ webView: WKWebView,
539
+        decidePolicyFor navigationAction: WKNavigationAction,
540
+        decisionHandler: @escaping (WKNavigationActionPolicy) -> Void
541
+    ) {
542
+        guard let url = navigationAction.request.url else {
543
+            decisionHandler(.allow)
544
+            return
545
+        }
546
+        let scheme = (url.scheme ?? "").lowercased()
547
+        if scheme == "http" || scheme == "https" {
548
+            decisionHandler(.allow)
549
+            return
550
+        }
551
+        NSWorkspace.shared.open(url)
552
+        decisionHandler(.cancel)
553
+    }
554
+
555
+    func webView(
556
+        _ webView: WKWebView,
557
+        createWebViewWith configuration: WKWebViewConfiguration,
558
+        for navigationAction: WKNavigationAction,
559
+        windowFeatures: WKWindowFeatures
560
+    ) -> WKWebView? {
561
+        if navigationAction.targetFrame == nil, let requestURL = navigationAction.request.url {
562
+            let scheme = (requestURL.scheme ?? "").lowercased()
563
+            if scheme == "http" || scheme == "https" {
564
+                webView.load(URLRequest(url: requestURL))
565
+            } else {
566
+                NSWorkspace.shared.open(requestURL)
567
+            }
568
+        }
569
+        return nil
570
+    }
571
+}
572
+

+ 30 - 13
classroom_app/ViewController.swift

@@ -577,7 +577,7 @@ private extension ViewController {
577 577
             return
578 578
         }
579 579
 
580
-        openInDefaultBrowser(url: url)
580
+        openURL(url)
581 581
     }
582 582
 
583 583
     @objc private func joinWithLinkCardClicked(_ sender: NSClickGestureRecognizer) {
@@ -599,27 +599,27 @@ private extension ViewController {
599 599
             showSimpleAlert(title: "Invalid address", message: "Enter a valid http or https URL.")
600 600
             return
601 601
         }
602
-        openInAppBrowser(with: url, policy: inAppBrowserDefaultPolicy)
602
+        openURL(url)
603 603
     }
604 604
 
605 605
     @objc private func browseQuickLinkMeetClicked(_ sender: Any?) {
606 606
         guard let url = URL(string: "https://classroom.google.com/") else { return }
607
-        openInDefaultBrowser(url: url)
607
+        openURL(url)
608 608
     }
609 609
 
610 610
     @objc private func browseQuickLinkMeetHelpClicked(_ sender: Any?) {
611 611
         guard let url = URL(string: "https://support.google.com/classroom") else { return }
612
-        openInAppBrowser(with: url, policy: inAppBrowserDefaultPolicy)
612
+        openURL(url)
613 613
     }
614 614
 
615 615
     @objc private func browseQuickLinkZoomHelpClicked(_ sender: Any?) {
616 616
         guard let url = URL(string: "https://support.zoom.us") else { return }
617
-        openInAppBrowser(with: url, policy: inAppBrowserDefaultPolicy)
617
+        openURL(url)
618 618
     }
619 619
 
620 620
     @objc private func instantMeetClicked(_ sender: NSClickGestureRecognizer) {
621 621
         guard let url = URL(string: "https://classroom.google.com/") else { return }
622
-        openInDefaultBrowser(url: url)
622
+        openURL(url)
623 623
     }
624 624
 
625 625
     private func normalizedURLString(from value: String) -> String {
@@ -705,6 +705,15 @@ private extension ViewController {
705 705
         }
706 706
     }
707 707
 
708
+    private func openURLWithRouting(_ url: URL, policy: InAppBrowserURLPolicy = .allowAll) {
709
+        let scheme = (url.scheme ?? "").lowercased()
710
+        if scheme == "http" || scheme == "https" {
711
+            openInAppBrowser(with: url, policy: policy)
712
+            return
713
+        }
714
+        openInDefaultBrowser(url: url)
715
+    }
716
+
708 717
     private func openRateUsDestination() {
709 718
         let configured = (Bundle.main.object(forInfoDictionaryKey: "RateUsURL") as? String)?
710 719
             .trimmingCharacters(in: .whitespacesAndNewlines)
@@ -767,7 +776,7 @@ private extension ViewController {
767 776
             showSimpleAlert(title: "Unable to Open Subscriptions", message: "The subscriptions URL is invalid.")
768 777
             return
769 778
         }
770
-        openInDefaultBrowser(url: url)
779
+        openURL(url)
771 780
     }
772 781
 
773 782
     private func shareAppURL() -> URL? {
@@ -895,7 +904,7 @@ private extension ViewController {
895 904
         case .moreApps:
896 905
             if let moreAppsURL = Bundle.main.object(forInfoDictionaryKey: "MoreAppsURL") as? String,
897 906
                let url = URL(string: moreAppsURL) {
898
-                openInAppBrowser(with: url, policy: inAppBrowserDefaultPolicy)
907
+                openURL(url)
899 908
             }
900 909
         case .shareApp:
901 910
             shareAppFromSettingsMenu(sourceView: sourceView, clickLocationInSourceView: clickLocationInSourceView)
@@ -908,7 +917,7 @@ private extension ViewController {
908 917
         let defaultURL = (Bundle.main.object(forInfoDictionaryKey: "AppLaunchPlaceholderURL") as? String) ?? "https://example.com/app-link-coming-soon"
909 918
         let urlString = (Bundle.main.object(forInfoDictionaryKey: infoKey) as? String) ?? defaultURL
910 919
         guard let url = URL(string: urlString) else { return }
911
-        openInAppBrowser(with: url, policy: inAppBrowserDefaultPolicy)
920
+        openURL(url)
912 921
     }
913 922
 
914 923
     private func showSimpleAlert(title: String, message: String) {
@@ -1113,7 +1122,7 @@ private extension ViewController {
1113 1122
             "Terms of Services": (Bundle.main.object(forInfoDictionaryKey: "TermsOfServiceURL") as? String) ?? defaultURL
1114 1123
         ]
1115 1124
         if let urlString = map[text], let url = URL(string: urlString) {
1116
-            openInAppBrowser(with: url, policy: inAppBrowserDefaultPolicy)
1125
+            openURL(url)
1117 1126
             return
1118 1127
         }
1119 1128
         showSimpleAlert(title: text, message: "\(text) tapped.")
@@ -6710,7 +6719,7 @@ private extension ViewController {
6710 6719
     }
6711 6720
 
6712 6721
     private func openURL(_ url: URL) {
6713
-        NSWorkspace.shared.open(url)
6722
+        openURLWithRouting(url, policy: inAppBrowserDefaultPolicy)
6714 6723
     }
6715 6724
 
6716 6725
     private func renderEnrolledClassCards(_ courses: [ClassroomCourse]) {
@@ -8273,6 +8282,10 @@ private final class InAppBrowserContainerViewController: NSViewController, WKNav
8273 8282
         alert.runModal()
8274 8283
     }
8275 8284
 
8285
+    private func openExternally(_ url: URL) {
8286
+        NSWorkspace.shared.open(url)
8287
+    }
8288
+
8276 8289
     func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
8277 8290
         processTerminateRetryCount = 0
8278 8291
         syncAddressFieldFromWebView()
@@ -8326,7 +8339,8 @@ private final class InAppBrowserContainerViewController: NSViewController, WKNav
8326 8339
             return
8327 8340
         }
8328 8341
         let scheme = (url.scheme ?? "").lowercased()
8329
-        if scheme == "mailto" || scheme == "tel" {
8342
+        if scheme != "http" && scheme != "https" {
8343
+            openExternally(url)
8330 8344
             decisionHandler(.cancel)
8331 8345
             return
8332 8346
         }
@@ -8349,7 +8363,10 @@ private final class InAppBrowserContainerViewController: NSViewController, WKNav
8349 8363
         windowFeatures: WKWindowFeatures
8350 8364
     ) -> WKWebView? {
8351 8365
         if navigationAction.targetFrame == nil, let requestURL = navigationAction.request.url {
8352
-            if inAppBrowserURLAllowed(requestURL, policy: navigationPolicy) {
8366
+            let scheme = (requestURL.scheme ?? "").lowercased()
8367
+            if scheme != "http" && scheme != "https" {
8368
+                openExternally(requestURL)
8369
+            } else if inAppBrowserURLAllowed(requestURL, policy: navigationPolicy) {
8353 8370
                 webView.load(URLRequest(url: requestURL))
8354 8371
             } else {
8355 8372
                 presentBlockedHostAlert()