Просмотр исходного кода

Fix Zoom/Google OAuth window sizing and sign-in UI reset

Align the in-app OAuth NSWindow content rect with the host window using
shared AppWindowMetrics (1020×690) and frameRect(forContentRect:).

Attach OAuth as a child window, reuse fixed content min/max after layout.

When the user closes the OAuth window, resume the pending continuation with
authenticationCancelled so the main thread can reset the top bar and Google
button instead of waiting for the two-minute timeout.

Map cancellation through ZoomOAuthService, skip error alerts for cancel on
primary sign-in and schedule flows, and reset the Google chip label in
resetLoginSigningInState.

Co-authored-by: Cursor <cursoragent@cursor.com>
huzaifahayat12 2 месяцев назад
Родитель
Сommit
cd8da569db
1 измененных файлов с 111 добавлено и 26 удалено
  1. 111 26
      zoom_app/ViewController.swift

+ 111 - 26
zoom_app/ViewController.swift

@@ -10,6 +10,12 @@ import CryptoKit
 import StoreKit
 import StoreKit
 import WebKit
 import WebKit
 
 
+/// Default main window **content** size; OAuth sign-in uses the same dimensions and aligns to this content area.
+private enum AppWindowMetrics {
+    static let defaultContentWidth: CGFloat = 1020
+    static let defaultContentHeight: CGFloat = 690
+}
+
 class ViewController: NSViewController {
 class ViewController: NSViewController {
     enum HomeSection: String {
     enum HomeSection: String {
         case home = "Home"
         case home = "Home"
@@ -532,7 +538,9 @@ class ViewController: NSViewController {
     override func viewDidAppear() {
     override func viewDidAppear() {
         super.viewDidAppear()
         super.viewDidAppear()
         if let window = view.window {
         if let window = view.window {
-            window.setContentSize(NSSize(width: 1020, height: 690))
+            window.setContentSize(
+                NSSize(width: AppWindowMetrics.defaultContentWidth, height: AppWindowMetrics.defaultContentHeight)
+            )
             applyWindowBackgroundForCurrentTheme(window)
             applyWindowBackgroundForCurrentTheme(window)
             // Use full-size content view so custom top chrome sits in the titlebar region.
             // Use full-size content view so custom top chrome sits in the titlebar region.
             window.titleVisibility = .hidden
             window.titleVisibility = .hidden
@@ -676,9 +684,8 @@ class ViewController: NSViewController {
                 }
                 }
             } catch {
             } catch {
                 await MainActor.run {
                 await MainActor.run {
-                    self.isSigningIn = false
-                    self.googleButton?.title = "G"
-                    self.googleButton?.isEnabled = true
+                    self.resetLoginSigningInState()
+                    if case GoogleOAuthError.authenticationCancelled = error { return }
                     self.showSimpleError("Google login failed", error: error)
                     self.showSimpleError("Google login failed", error: error)
                 }
                 }
             }
             }
@@ -714,6 +721,7 @@ class ViewController: NSViewController {
             } catch {
             } catch {
                 await MainActor.run {
                 await MainActor.run {
                     self.resetLoginSigningInState()
                     self.resetLoginSigningInState()
+                    if case ZoomOAuthError.authenticationCancelled = error { return }
                     self.showSimpleError("Zoom sign-in failed", error: error)
                     self.showSimpleError("Zoom sign-in failed", error: error)
                 }
                 }
             }
             }
@@ -1023,7 +1031,8 @@ class ViewController: NSViewController {
                     await MainActor.run { self.scheduleSubmitButton?.isEnabled = true }
                     await MainActor.run { self.scheduleSubmitButton?.isEnabled = true }
                     return
                     return
                 }
                 }
-                let token = try await zoomOAuth.validAccessToken(presentingWindow: view.window)
+                let oauthParent = await MainActor.run { self.scheduleMeetingWindow ?? self.view.window }
+                let token = try await zoomOAuth.validAccessToken(presentingWindow: oauthParent)
                 let result = try await createZoomMeeting(
                 let result = try await createZoomMeeting(
                     accessToken: token,
                     accessToken: token,
                     topic: topic,
                     topic: topic,
@@ -1063,6 +1072,8 @@ class ViewController: NSViewController {
                         let seconds = max(retryAfter ?? 300, 30)
                         let seconds = max(retryAfter ?? 300, 30)
                         let minutes = Int(ceil(Double(seconds) / 60.0))
                         let minutes = Int(ceil(Double(seconds) / 60.0))
                         self.showSimpleAlert(title: "Rate limited", message: "Zoom asked to wait before scheduling again. Try in about \(minutes) min.")
                         self.showSimpleAlert(title: "Rate limited", message: "Zoom asked to wait before scheduling again. Try in about \(minutes) min.")
+                    } else if case ZoomOAuthError.authenticationCancelled = error {
+                        // User closed the sign-in window; no alert.
                     } else {
                     } else {
                         self.showSimpleError("Could not schedule", error: error)
                         self.showSimpleError("Could not schedule", error: error)
                     }
                     }
@@ -2046,6 +2057,7 @@ class ViewController: NSViewController {
     @MainActor
     @MainActor
     private func resetLoginSigningInState() {
     private func resetLoginSigningInState() {
         isSigningIn = false
         isSigningIn = false
+        googleButton?.title = "G"
         updateTopBarAuthButton()
         updateTopBarAuthButton()
         googleButton?.isEnabled = true
         googleButton?.isEnabled = true
     }
     }
@@ -6598,6 +6610,7 @@ enum ZoomOAuthError: Error {
     case rateLimited(retryAfterSeconds: Int?)
     case rateLimited(retryAfterSeconds: Int?)
     case unableToOpenBrowser
     case unableToOpenBrowser
     case authenticationTimedOut
     case authenticationTimedOut
+    case authenticationCancelled
 }
 }
 
 
 final class ZoomOAuthTokenStore {
 final class ZoomOAuthTokenStore {
@@ -6690,7 +6703,7 @@ final class ZoomOAuthService: NSObject {
     }
     }
 
 
     private func interactiveSignIn(presentingWindow: NSWindow?) async throws -> ZoomOAuthTokens {
     private func interactiveSignIn(presentingWindow: NSWindow?) async throws -> ZoomOAuthTokens {
-        _ = presentingWindow
+        guard let presentingWindow else { throw ZoomOAuthError.unableToOpenBrowser }
         let oauthWindow = await MainActor.run { makeOAuthWindowController() }
         let oauthWindow = await MainActor.run { makeOAuthWindowController() }
         guard let clientId = configuredClientId() else { throw ZoomOAuthError.missingClientId }
         guard let clientId = configuredClientId() else { throw ZoomOAuthError.missingClientId }
         guard let clientSecret = configuredClientSecret() else { throw ZoomOAuthError.missingClientSecret }
         guard let clientSecret = configuredClientSecret() else { throw ZoomOAuthError.missingClientSecret }
@@ -6709,7 +6722,11 @@ final class ZoomOAuthService: NSObject {
         ]
         ]
         guard let authURL = components.url else { throw ZoomOAuthError.invalidCallbackURL }
         guard let authURL = components.url else { throw ZoomOAuthError.invalidCallbackURL }
         do {
         do {
-            let callbackURL = try await oauthWindow.authenticate(startURL: authURL, callbackURLPrefix: redirectURI)
+            let callbackURL = try await oauthWindow.authenticate(
+                startURL: authURL,
+                callbackURLPrefix: redirectURI,
+                presentingWindow: presentingWindow
+            )
             let queryItems = URLComponents(url: callbackURL, resolvingAgainstBaseURL: false)?.queryItems
             let queryItems = URLComponents(url: callbackURL, resolvingAgainstBaseURL: false)?.queryItems
             guard queryItems?.first(where: { $0.name == "state" })?.value == state else { throw ZoomOAuthError.invalidCallbackURL }
             guard queryItems?.first(where: { $0.name == "state" })?.value == state else { throw ZoomOAuthError.invalidCallbackURL }
             guard let code = queryItems?.first(where: { $0.name == "code" })?.value, code.isEmpty == false else {
             guard let code = queryItems?.first(where: { $0.name == "code" })?.value, code.isEmpty == false else {
@@ -6718,6 +6735,8 @@ final class ZoomOAuthService: NSObject {
             return try await exchangeCodeForTokens(code: code, redirectURI: redirectURI, clientId: clientId, clientSecret: clientSecret)
             return try await exchangeCodeForTokens(code: code, redirectURI: redirectURI, clientId: clientId, clientSecret: clientSecret)
         } catch GoogleOAuthError.authenticationTimedOut {
         } catch GoogleOAuthError.authenticationTimedOut {
             throw ZoomOAuthError.authenticationTimedOut
             throw ZoomOAuthError.authenticationTimedOut
+        } catch GoogleOAuthError.authenticationCancelled {
+            throw ZoomOAuthError.authenticationCancelled
         } catch {
         } catch {
             throw error
             throw error
         }
         }
@@ -6841,6 +6860,7 @@ enum GoogleOAuthError: Error {
     case tokenExchangeFailed(String)
     case tokenExchangeFailed(String)
     case unableToOpenBrowser
     case unableToOpenBrowser
     case authenticationTimedOut
     case authenticationTimedOut
+    case authenticationCancelled
 }
 }
 
 
 final class GoogleOAuthService: NSObject {
 final class GoogleOAuthService: NSObject {
@@ -6881,7 +6901,7 @@ final class GoogleOAuthService: NSObject {
     }
     }
 
 
     private func interactiveSignIn(presentingWindow: NSWindow?) async throws -> GoogleOAuthTokens {
     private func interactiveSignIn(presentingWindow: NSWindow?) async throws -> GoogleOAuthTokens {
-        _ = presentingWindow
+        guard let presentingWindow else { throw GoogleOAuthError.unableToOpenBrowser }
         let oauthWindow = await MainActor.run { makeOAuthWindowController() }
         let oauthWindow = await MainActor.run { makeOAuthWindowController() }
         let codeVerifier = Self.randomURLSafeString(length: 64)
         let codeVerifier = Self.randomURLSafeString(length: 64)
         let codeChallenge = Self.pkceChallenge(for: codeVerifier)
         let codeChallenge = Self.pkceChallenge(for: codeVerifier)
@@ -6903,7 +6923,11 @@ final class GoogleOAuthService: NSObject {
         ]
         ]
 
 
         guard let authURL = components.url else { throw GoogleOAuthError.invalidCallbackURL }
         guard let authURL = components.url else { throw GoogleOAuthError.invalidCallbackURL }
-        let callbackURL = try await oauthWindow.authenticate(startURL: authURL, callbackURLPrefix: redirectURI)
+        let callbackURL = try await oauthWindow.authenticate(
+            startURL: authURL,
+            callbackURLPrefix: redirectURI,
+            presentingWindow: presentingWindow
+        )
         let query = URLComponents(url: callbackURL, resolvingAgainstBaseURL: false)?.queryItems
         let query = URLComponents(url: callbackURL, resolvingAgainstBaseURL: false)?.queryItems
         guard query?.first(where: { $0.name == "state" })?.value == state else { throw GoogleOAuthError.invalidCallbackURL }
         guard query?.first(where: { $0.name == "state" })?.value == state else { throw GoogleOAuthError.invalidCallbackURL }
         guard let code = query?.first(where: { $0.name == "code" })?.value, code.isEmpty == false else {
         guard let code = query?.first(where: { $0.name == "code" })?.value, code.isEmpty == false else {
@@ -7029,56 +7053,114 @@ private final class OAuthWebViewContainerView: NSView {
     }
     }
 }
 }
 
 
+/// Separate titled window matching the app’s default content size (see `AppWindowMetrics`); when a parent window is provided, the OAuth content area is aligned to the parent’s content rect.
 @MainActor
 @MainActor
-private final class InAppOAuthWindowController: NSWindowController, WKNavigationDelegate {
+private final class InAppOAuthWindowController: NSWindowController, WKNavigationDelegate, NSWindowDelegate {
     private let webView: WKWebView
     private let webView: WKWebView
     private var callbackPrefix: String?
     private var callbackPrefix: String?
     private var callbackContinuation: CheckedContinuation<URL, Error>?
     private var callbackContinuation: CheckedContinuation<URL, Error>?
+    private weak var parentWindowForChild: NSWindow?
+    /// When true, `windowWillClose` does not treat the close as user cancellation (we closed after success/timeout/error).
+    private var suppressUserCloseCancellation = false
 
 
     init(title: String) {
     init(title: String) {
         let configuration = WKWebViewConfiguration()
         let configuration = WKWebViewConfiguration()
         configuration.websiteDataStore = .nonPersistent()
         configuration.websiteDataStore = .nonPersistent()
         self.webView = WKWebView(frame: .zero, configuration: configuration)
         self.webView = WKWebView(frame: .zero, configuration: configuration)
         let container = OAuthWebViewContainerView(webView: webView)
         let container = OAuthWebViewContainerView(webView: webView)
-        let window = NSWindow(contentRect: NSRect(x: 0, y: 0, width: 980, height: 760), styleMask: [.titled, .closable, .miniaturizable, .resizable], backing: .buffered, defer: false)
+        let contentSize = NSSize(width: AppWindowMetrics.defaultContentWidth, height: AppWindowMetrics.defaultContentHeight)
+        let window = NSWindow(contentRect: NSRect(origin: .zero, size: contentSize), styleMask: [.titled, .closable, .miniaturizable], backing: .buffered, defer: false)
         window.title = title
         window.title = title
         window.contentView = container
         window.contentView = container
+        window.contentMinSize = contentSize
+        window.contentMaxSize = contentSize
+        window.isReleasedWhenClosed = false
         super.init(window: window)
         super.init(window: window)
+        window.delegate = self
         webView.navigationDelegate = self
         webView.navigationDelegate = self
     }
     }
 
 
     @available(*, unavailable) required init?(coder: NSCoder) { nil }
     @available(*, unavailable) required init?(coder: NSCoder) { nil }
 
 
-    func authenticate(startURL: URL, callbackURLPrefix: String, timeoutSeconds: Double = 120) async throws -> URL {
+    func windowWillClose(_ notification: Notification) {
+        guard suppressUserCloseCancellation == false else { return }
+        removeFromParentIfNeeded()
+        webView.stopLoading()
+        if let continuation = callbackContinuation {
+            callbackContinuation = nil
+            continuation.resume(throwing: GoogleOAuthError.authenticationCancelled)
+        }
+    }
+
+    func authenticate(startURL: URL, callbackURLPrefix: String, presentingWindow: NSWindow?, timeoutSeconds: Double = 120) async throws -> URL {
         self.callbackPrefix = callbackURLPrefix
         self.callbackPrefix = callbackURLPrefix
+        parentWindowForChild = presentingWindow
+        applyFixedOAuthFrame(presentingWindow: presentingWindow)
+        if let parent = presentingWindow, let w = window {
+            parent.addChildWindow(w, ordered: .above)
+        }
         showWindow(nil)
         showWindow(nil)
         window?.makeKeyAndOrderFront(nil)
         window?.makeKeyAndOrderFront(nil)
-        window?.center()
         webView.load(URLRequest(url: startURL))
         webView.load(URLRequest(url: startURL))
 
 
-        return try await withThrowingTaskGroup(of: URL.self) { group in
-            group.addTask { [weak self] in
-                guard let self else { throw GoogleOAuthError.invalidCallbackURL }
-                return try await self.awaitCallback()
-            }
-            group.addTask {
-                try await Task.sleep(nanoseconds: UInt64(timeoutSeconds * 1_000_000_000))
-                throw GoogleOAuthError.authenticationTimedOut
+        do {
+            return try await withThrowingTaskGroup(of: URL.self) { group in
+                group.addTask { [weak self] in
+                    guard let self else { throw GoogleOAuthError.invalidCallbackURL }
+                    return try await self.awaitCallback()
+                }
+                group.addTask {
+                    try await Task.sleep(nanoseconds: UInt64(timeoutSeconds * 1_000_000_000))
+                    throw GoogleOAuthError.authenticationTimedOut
+                }
+                let url = try await group.next()!
+                group.cancelAll()
+                self.closeOAuthWindowAfterFlow()
+                return url
             }
             }
-            let url = try await group.next()!
-            group.cancelAll()
-            self.webView.stopLoading()
-            self.close()
-            return url
+        } catch {
+            self.closeOAuthWindowAfterFlow()
+            throw error
         }
         }
     }
     }
 
 
+    private func closeOAuthWindowAfterFlow() {
+        suppressUserCloseCancellation = true
+        webView.stopLoading()
+        removeFromParentIfNeeded()
+        close()
+        suppressUserCloseCancellation = false
+    }
+
     private func awaitCallback() async throws -> URL {
     private func awaitCallback() async throws -> URL {
         try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<URL, Error>) in
         try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<URL, Error>) in
             callbackContinuation = continuation
             callbackContinuation = continuation
         }
         }
     }
     }
 
 
+    private func removeFromParentIfNeeded() {
+        if let parent = parentWindowForChild, let w = window {
+            parent.removeChildWindow(w)
+        }
+        parentWindowForChild = nil
+    }
+
+    private func applyFixedOAuthFrame(presentingWindow: NSWindow?) {
+        guard let window else { return }
+        if let parent = presentingWindow {
+            let parentContent = parent.contentRect(forFrameRect: parent.frame)
+            let oauthFrame = window.frameRect(forContentRect: parentContent)
+            window.setFrame(oauthFrame, display: true)
+        } else {
+            let contentSize = NSSize(width: AppWindowMetrics.defaultContentWidth, height: AppWindowMetrics.defaultContentHeight)
+            window.setContentSize(contentSize)
+            window.center()
+        }
+        let lockedContentSize = window.contentRect(forFrameRect: window.frame).size
+        window.contentMinSize = lockedContentSize
+        window.contentMaxSize = lockedContentSize
+    }
+
     func webView(_ webView: WKWebView,
     func webView(_ webView: WKWebView,
                  decidePolicyFor navigationAction: WKNavigationAction,
                  decidePolicyFor navigationAction: WKNavigationAction,
                  decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
                  decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
@@ -7105,6 +7187,7 @@ extension GoogleOAuthError: LocalizedError {
         case .tokenExchangeFailed(let details): return "Token exchange failed: \(details)"
         case .tokenExchangeFailed(let details): return "Token exchange failed: \(details)"
         case .unableToOpenBrowser: return "Could not open browser for Google sign-in."
         case .unableToOpenBrowser: return "Could not open browser for Google sign-in."
         case .authenticationTimedOut: return "Google sign-in timed out."
         case .authenticationTimedOut: return "Google sign-in timed out."
+        case .authenticationCancelled: return "Sign-in was cancelled."
         }
         }
     }
     }
 }
 }
@@ -7133,6 +7216,8 @@ extension ZoomOAuthError: LocalizedError {
             return "Could not open the system browser for Zoom sign-in."
             return "Could not open the system browser for Zoom sign-in."
         case .authenticationTimedOut:
         case .authenticationTimedOut:
             return "Zoom sign-in timed out waiting for the browser redirect."
             return "Zoom sign-in timed out waiting for the browser redirect."
+        case .authenticationCancelled:
+            return "Zoom sign-in was cancelled."
         }
         }
     }
     }
 }
 }