Selaa lähdekoodia

Route web links through embedded panel and move Google OAuth into an in-app sign-in window.

This mirrors classroom-style navigation so http(s) links stay inside the app while preserving external handling for non-web schemes.

Made-with: Cursor
huzaifahayat12 3 kuukautta sitten
vanhempi
sitoutus
41da06688e
2 muutettua tiedostoa jossa 255 lisäystä ja 23 poistoa
  1. 154 2
      meetings_app/Auth/GoogleOAuthService.swift
  2. 101 21
      meetings_app/ViewController.swift

+ 154 - 2
meetings_app/Auth/GoogleOAuthService.swift

@@ -2,6 +2,7 @@ import Foundation
 import CryptoKit
 import AppKit
 import Network
+import WebKit
 
 struct GoogleOAuthTokens: Codable, Equatable {
     var accessToken: String
@@ -50,6 +51,7 @@ final class GoogleOAuthService: NSObject {
     ]
 
     private let tokenStore = KeychainTokenStore()
+    @MainActor private var inAppOAuthWindowController: InAppOAuthWindowController?
     private override init() {}
 
     func configuredClientId() -> String? {
@@ -127,7 +129,6 @@ final class GoogleOAuthService: NSObject {
     // MARK: - Interactive sign-in (Authorization Code + PKCE)
 
     private func interactiveSignIn(presentingWindow: NSWindow?) async throws -> GoogleOAuthTokens {
-        _ = presentingWindow
         guard let clientId = configuredClientId() else { throw GoogleOAuthError.missingClientId }
         guard let clientSecret = configuredClientSecret() else { throw GoogleOAuthError.missingClientSecret }
         let codeVerifier = Self.randomURLSafeString(length: 64)
@@ -153,7 +154,15 @@ final class GoogleOAuthService: NSObject {
         ]
 
         guard let authURL = components.url else { throw GoogleOAuthError.invalidCallbackURL }
-        guard NSWorkspace.shared.open(authURL) else { throw GoogleOAuthError.unableToOpenBrowser }
+        let opened = await MainActor.run { [self] in
+            openAuthURLInApp(authURL, presentingWindow: presentingWindow)
+        }
+        guard opened else { throw GoogleOAuthError.unableToOpenBrowser }
+        defer {
+            Task { @MainActor [weak self] in
+                self?.closeInAppOAuthWindow()
+            }
+        }
         let callbackURL = try await loopback.waitForCallback()
 
         guard let returnedState = URLComponents(url: callbackURL, resolvingAgainstBaseURL: false)?
@@ -177,6 +186,30 @@ final class GoogleOAuthService: NSObject {
         )
     }
 
+    @MainActor
+    private func openAuthURLInApp(_ url: URL, presentingWindow: NSWindow?) -> Bool {
+        let controller: InAppOAuthWindowController
+        if let existing = inAppOAuthWindowController {
+            controller = existing
+        } else {
+            controller = InAppOAuthWindowController()
+            inAppOAuthWindowController = controller
+        }
+        controller.alignWithPresentingWindow(presentingWindow)
+        controller.load(url: url)
+        controller.showWindow(nil)
+        controller.window?.makeKeyAndOrderFront(nil)
+        controller.window?.orderFrontRegardless()
+        NSApp.activate(ignoringOtherApps: true)
+        return true
+    }
+
+    @MainActor
+    private func closeInAppOAuthWindow() {
+        inAppOAuthWindowController?.close()
+        inAppOAuthWindowController = nil
+    }
+
     private func exchangeCodeForTokens(code: String, codeVerifier: String, redirectURI: String, clientId: String, clientSecret: String) async throws -> GoogleOAuthTokens {
         var request = URLRequest(url: URL(string: "https://oauth2.googleapis.com/token")!)
         request.httpMethod = "POST"
@@ -481,3 +514,122 @@ extension GoogleOAuthError: LocalizedError {
     }
 }
 
+@MainActor
+private final class OAuthWebViewContainerView: NSView {
+    private let webView: WKWebView
+
+    init(webView: WKWebView) {
+        self.webView = webView
+        super.init(frame: .zero)
+        autoresizingMask = [.width, .height]
+        addSubview(webView)
+    }
+
+    @available(*, unavailable)
+    required init?(coder: NSCoder) {
+        nil
+    }
+
+    override func layout() {
+        super.layout()
+        webView.frame = bounds
+    }
+}
+
+@MainActor
+private final class InAppOAuthWindowController: NSWindowController, WKNavigationDelegate, WKUIDelegate {
+    private let webView: WKWebView
+    private let defaultWindowSize = NSSize(width: 980, height: 760)
+
+    init() {
+        let config = WKWebViewConfiguration()
+        if #available(macOS 11.0, *) {
+            config.defaultWebpagePreferences.allowsContentJavaScript = true
+        }
+        self.webView = WKWebView(frame: .zero, configuration: config)
+        let container = OAuthWebViewContainerView(webView: webView)
+
+        let window = NSWindow(
+            contentRect: NSRect(origin: .zero, size: defaultWindowSize),
+            styleMask: [.titled, .closable, .miniaturizable, .resizable],
+            backing: .buffered,
+            defer: false
+        )
+        window.title = "Google Sign-In"
+        window.contentView = container
+        window.center()
+        super.init(window: window)
+        webView.navigationDelegate = self
+        webView.uiDelegate = self
+    }
+
+    @available(*, unavailable)
+    required init?(coder: NSCoder) {
+        nil
+    }
+
+    func load(url: URL) {
+        webView.load(URLRequest(url: url))
+    }
+
+    func alignWithPresentingWindow(_ presentingWindow: NSWindow?) {
+        guard let window else { return }
+        if let presentingWindow {
+            window.setFrame(presentingWindow.frame, display: false)
+            return
+        }
+        if let screenFrame = NSScreen.main?.visibleFrame {
+            let origin = NSPoint(
+                x: screenFrame.midX - (defaultWindowSize.width / 2),
+                y: screenFrame.midY - (defaultWindowSize.height / 2)
+            )
+            window.setFrame(NSRect(origin: origin, size: defaultWindowSize), display: false)
+        } else {
+            window.center()
+        }
+    }
+
+    private func shouldOpenURLExternally(_ url: URL) -> Bool {
+        let scheme = (url.scheme ?? "").lowercased()
+        guard !scheme.isEmpty else { return false }
+        return scheme != "about" && scheme != "javascript"
+    }
+
+    func webView(
+        _ webView: WKWebView,
+        decidePolicyFor navigationAction: WKNavigationAction,
+        decisionHandler: @escaping (WKNavigationActionPolicy) -> Void
+    ) {
+        guard let url = navigationAction.request.url else {
+            decisionHandler(.allow)
+            return
+        }
+        let scheme = (url.scheme ?? "").lowercased()
+        if scheme == "http" || scheme == "https" {
+            decisionHandler(.allow)
+            return
+        }
+        if shouldOpenURLExternally(url) {
+            NSWorkspace.shared.open(url)
+        }
+        decisionHandler(.cancel)
+    }
+
+    func webView(
+        _ webView: WKWebView,
+        createWebViewWith configuration: WKWebViewConfiguration,
+        for navigationAction: WKNavigationAction,
+        windowFeatures: WKWindowFeatures
+    ) -> WKWebView? {
+        if navigationAction.targetFrame == nil, let requestURL = navigationAction.request.url {
+            let scheme = (requestURL.scheme ?? "").lowercased()
+            if scheme == "http" || scheme == "https" {
+                webView.load(URLRequest(url: requestURL))
+            } else if shouldOpenURLExternally(requestURL) {
+                NSWorkspace.shared.open(requestURL)
+            }
+        }
+        return nil
+    }
+}
+

+ 101 - 21
meetings_app/ViewController.swift

@@ -287,6 +287,12 @@ final class ViewController: NSViewController {
     private weak var meetLinkField: NSTextField?
     private weak var browseAddressField: NSTextField?
     private var inAppBrowserWindowController: InAppBrowserWindowController?
+    private var embeddedBrowserViewController: InAppBrowserContainerViewController?
+    private var embeddedBrowserURL: URL?
+    private var embeddedBrowserPolicy: InAppBrowserURLPolicy = .allowAll
+    private weak var mainPanelAuthBar: NSView?
+    private var mainContentHostTopToAuthConstraint: NSLayoutConstraint?
+    private var mainContentHostTopToPanelConstraint: NSLayoutConstraint?
     private let googleOAuth = GoogleOAuthService.shared
     private let calendarClient = GoogleCalendarClient()
     private let storeKitCoordinator = StoreKitCoordinator()
@@ -712,7 +718,7 @@ private extension ViewController {
             showSimpleAlert(title: "Invalid address", message: "Enter a valid http or https URL.")
             return
         }
-        openInAppBrowser(with: url, policy: inAppBrowserDefaultPolicy)
+        openURLWithRouting(url, policy: inAppBrowserDefaultPolicy)
     }
 
     @objc private func browseQuickLinkMeetClicked(_ sender: Any?) {
@@ -722,12 +728,12 @@ private extension ViewController {
 
     @objc private func browseQuickLinkMeetHelpClicked(_ sender: Any?) {
         guard let url = URL(string: "https://support.google.com/meet") else { return }
-        openInAppBrowser(with: url, policy: inAppBrowserDefaultPolicy)
+        openURLWithRouting(url, policy: inAppBrowserDefaultPolicy)
     }
 
     @objc private func browseQuickLinkZoomHelpClicked(_ sender: Any?) {
         guard let url = URL(string: "https://support.zoom.us") else { return }
-        openInAppBrowser(with: url, policy: inAppBrowserDefaultPolicy)
+        openURLWithRouting(url, policy: inAppBrowserDefaultPolicy)
     }
 
     @objc private func instantMeetClicked(_ sender: NSClickGestureRecognizer) {
@@ -812,6 +818,77 @@ private extension ViewController {
         }
     }
 
+    private func openURLWithRouting(_ url: URL, policy: InAppBrowserURLPolicy = .allowAll) {
+        let scheme = (url.scheme ?? "").lowercased()
+        if scheme == "http" || scheme == "https" {
+            showEmbeddedWebPage(url, policy: policy)
+            return
+        }
+        openInDefaultBrowser(url: url)
+    }
+
+    private func embeddedBrowserController() -> InAppBrowserContainerViewController {
+        if let existing = embeddedBrowserViewController {
+            return existing
+        }
+        let controller = InAppBrowserContainerViewController()
+        embeddedBrowserViewController = controller
+        return controller
+    }
+
+    private func detachEmbeddedBrowserIfNeeded() {
+        guard let controller = embeddedBrowserViewController, controller.parent === self else { return }
+        controller.view.removeFromSuperview()
+        controller.removeFromParent()
+    }
+
+    private func mountMainContentView(_ child: NSView) {
+        guard let host = mainContentHost else { return }
+        NSLayoutConstraint.deactivate(mainContentHostPinConstraints)
+        mainContentHostPinConstraints.removeAll()
+        host.subviews.forEach { $0.removeFromSuperview() }
+        child.translatesAutoresizingMaskIntoConstraints = false
+        host.addSubview(child)
+        mainContentHostPinConstraints = [
+            child.leadingAnchor.constraint(equalTo: host.leadingAnchor),
+            child.trailingAnchor.constraint(equalTo: host.trailingAnchor),
+            child.topAnchor.constraint(equalTo: host.topAnchor),
+            child.bottomAnchor.constraint(equalTo: host.bottomAnchor)
+        ]
+        NSLayoutConstraint.activate(mainContentHostPinConstraints)
+    }
+
+    private func showEmbeddedWebPage(_ url: URL, policy: InAppBrowserURLPolicy = .allowAll) {
+        embeddedBrowserURL = url
+        embeddedBrowserPolicy = policy
+
+        let controller = embeddedBrowserController()
+        _ = controller.view
+        if controller.parent !== self {
+            addChild(controller)
+        }
+        controller.setNavigationPolicy(policy)
+        controller.load(url: url)
+
+        setEmbeddedBrowserLayoutMode(isEmbedded: true)
+        applyEmbeddedBrowserWindowTitle(url: url)
+        mountMainContentView(controller.view)
+    }
+
+    private func setEmbeddedBrowserLayoutMode(isEmbedded: Bool) {
+        mainPanelAuthBar?.isHidden = isEmbedded
+        mainContentHostTopToAuthConstraint?.isActive = !isEmbedded
+        mainContentHostTopToPanelConstraint?.isActive = isEmbedded
+    }
+
+    private func applyEmbeddedBrowserWindowTitle(url: URL) {
+        if let host = url.host, host.isEmpty == false {
+            view.window?.title = host
+        } else {
+            view.window?.title = "Browser"
+        }
+    }
+
     private func openRateUsDestination() {
         let configured = (Bundle.main.object(forInfoDictionaryKey: "RateUsURL") as? String)?
             .trimmingCharacters(in: .whitespacesAndNewlines)
@@ -938,21 +1015,10 @@ private extension ViewController {
         selectedSidebarPage = page
         updateSidebarAppearance()
         applyWindowTitle(for: page)
-
-        guard let host = mainContentHost else { return }
-        NSLayoutConstraint.deactivate(mainContentHostPinConstraints)
-        mainContentHostPinConstraints.removeAll()
-        host.subviews.forEach { $0.removeFromSuperview() }
+        setEmbeddedBrowserLayoutMode(isEmbedded: false)
+        detachEmbeddedBrowserIfNeeded()
         let child = viewForPage(page)
-        child.translatesAutoresizingMaskIntoConstraints = false
-        host.addSubview(child)
-        mainContentHostPinConstraints = [
-            child.leadingAnchor.constraint(equalTo: host.leadingAnchor),
-            child.trailingAnchor.constraint(equalTo: host.trailingAnchor),
-            child.topAnchor.constraint(equalTo: host.topAnchor),
-            child.bottomAnchor.constraint(equalTo: host.bottomAnchor)
-        ]
-        NSLayoutConstraint.activate(mainContentHostPinConstraints)
+        mountMainContentView(child)
     }
 
     private func showSettingsPopover() {
@@ -999,6 +1065,14 @@ private extension ViewController {
         paywallContinueButton = nil
         paywallContinueEnabled = true
 
+        detachEmbeddedBrowserIfNeeded()
+        embeddedBrowserViewController = nil
+        embeddedBrowserURL = nil
+        embeddedBrowserPolicy = .allowAll
+        mainPanelAuthBar = nil
+        mainContentHostTopToAuthConstraint = nil
+        mainContentHostTopToPanelConstraint = nil
+
         googleAccountPopover?.performClose(nil)
         googleAccountPopover = nil
 
@@ -1026,7 +1100,7 @@ private extension ViewController {
         case .moreApps:
             if let moreAppsURL = Bundle.main.object(forInfoDictionaryKey: "MoreAppsURL") as? String,
                let url = URL(string: moreAppsURL) {
-                openInAppBrowser(with: url, policy: inAppBrowserDefaultPolicy)
+                openURLWithRouting(url, policy: inAppBrowserDefaultPolicy)
             }
         case .shareApp:
             shareAppFromSettingsMenu(sourceView: sourceView, clickLocationInSourceView: clickLocationInSourceView)
@@ -1039,7 +1113,7 @@ private extension ViewController {
         let defaultURL = (Bundle.main.object(forInfoDictionaryKey: "AppLaunchPlaceholderURL") as? String) ?? "https://example.com/app-link-coming-soon"
         let urlString = (Bundle.main.object(forInfoDictionaryKey: infoKey) as? String) ?? defaultURL
         guard let url = URL(string: urlString) else { return }
-        openInAppBrowser(with: url, policy: inAppBrowserDefaultPolicy)
+        openURLWithRouting(url, policy: inAppBrowserDefaultPolicy)
     }
 
     private func showSimpleAlert(title: String, message: String) {
@@ -2427,17 +2501,23 @@ private extension ViewController {
         let host = NSView()
         host.translatesAutoresizingMaskIntoConstraints = false
         panel.addSubview(host)
+        let hostTopToAuth = host.topAnchor.constraint(equalTo: authBar.bottomAnchor, constant: 20)
+        let hostTopToPanel = host.topAnchor.constraint(equalTo: panel.topAnchor)
+        hostTopToPanel.isActive = false
         NSLayoutConstraint.activate([
             authBar.leadingAnchor.constraint(equalTo: panel.leadingAnchor, constant: 28),
             authBar.trailingAnchor.constraint(equalTo: panel.trailingAnchor, constant: -28),
-            authBar.topAnchor.constraint(equalTo: panel.topAnchor, constant: 26),
+            authBar.topAnchor.constraint(equalTo: panel.safeAreaLayoutGuide.topAnchor, constant: 26),
 
             host.leadingAnchor.constraint(equalTo: panel.leadingAnchor),
             host.trailingAnchor.constraint(equalTo: panel.trailingAnchor),
-            host.topAnchor.constraint(equalTo: authBar.bottomAnchor, constant: 20),
+            hostTopToAuth,
             host.bottomAnchor.constraint(equalTo: panel.bottomAnchor)
         ])
         mainContentHost = host
+        mainPanelAuthBar = authBar
+        mainContentHostTopToAuthConstraint = hostTopToAuth
+        mainContentHostTopToPanelConstraint = hostTopToPanel
 
         if hasGoogleSessionAvailable(), let profile = scheduleCurrentProfile {
             applyGoogleProfile(profile)