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

Fix Google account mismatch when opening Meet links

Route Google OAuth through the default browser and add account hints when opening Google URLs so Meet continues with the same signed-in account (or prompts login).

Co-authored-by: Cursor <cursoragent@cursor.com>
huzaifahayat12 2 месяцев назад
Родитель
Сommit
889274c388

+ 51 - 24
meetings_app/Auth/GoogleOAuthService.swift

@@ -55,6 +55,35 @@ final class GoogleOAuthService: NSObject {
     private let tokenStore = KeychainTokenStore()
     private let tokenStore = KeychainTokenStore()
     @MainActor private var inAppOAuthWindowController: InAppOAuthWindowController?
     @MainActor private var inAppOAuthWindowController: InAppOAuthWindowController?
     private override init() {}
     private override init() {}
+    
+    private let lastSignedInEmailDefaultsKey = "google.oauth.lastSignedInEmail"
+    
+    func lastSignedInEmailHint() -> String? {
+        let value = UserDefaults.standard
+            .string(forKey: lastSignedInEmailDefaultsKey)?
+            .trimmingCharacters(in: .whitespacesAndNewlines)
+        guard let value, value.isEmpty == false else { return nil }
+        return value.lowercased()
+    }
+    
+    /// Wraps a Google URL so the default browser is nudged to use the same account as the app.
+    /// If the browser isn't signed into that account, Google will prompt to sign in.
+    func urlForPreferredGoogleAccountIfPossible(_ url: URL) -> URL {
+        guard let email = lastSignedInEmailHint() else { return url }
+        guard let scheme = url.scheme?.lowercased(), scheme == "https" || scheme == "http" else { return url }
+        guard let host = url.host?.lowercased(), host.isEmpty == false else { return url }
+        
+        // Only attempt for Google properties where account mixups are common.
+        let isGoogle = host == "google.com" || host.hasSuffix(".google.com")
+        guard isGoogle else { return url }
+        
+        var components = URLComponents(string: "https://accounts.google.com/AccountChooser")!
+        components.queryItems = [
+            URLQueryItem(name: "continue", value: url.absoluteString),
+            URLQueryItem(name: "Email", value: email)
+        ]
+        return components.url ?? url
+    }
 
 
     func configuredClientId() -> String? {
     func configuredClientId() -> String? {
         let value = UserDefaults.standard.string(forKey: clientIdDefaultsKey)?.trimmingCharacters(in: .whitespacesAndNewlines)
         let value = UserDefaults.standard.string(forKey: clientIdDefaultsKey)?.trimmingCharacters(in: .whitespacesAndNewlines)
@@ -108,7 +137,12 @@ final class GoogleOAuthService: NSObject {
             let details = String(data: data, encoding: .utf8) ?? "HTTP \((response as? HTTPURLResponse)?.statusCode ?? -1)"
             let details = String(data: data, encoding: .utf8) ?? "HTTP \((response as? HTTPURLResponse)?.statusCode ?? -1)"
             throw GoogleOAuthError.tokenExchangeFailed(details)
             throw GoogleOAuthError.tokenExchangeFailed(details)
         }
         }
-        return try JSONDecoder().decode(GoogleUserProfile.self, from: data)
+        let profile = try JSONDecoder().decode(GoogleUserProfile.self, from: data)
+        let cleaned = profile.email?.trimmingCharacters(in: .whitespacesAndNewlines)
+        if let cleaned, cleaned.isEmpty == false {
+            UserDefaults.standard.set(cleaned.lowercased(), forKey: lastSignedInEmailDefaultsKey)
+        }
+        return profile
     }
     }
 
 
     func validAccessToken(presentingWindow: NSWindow?) async throws -> String {
     func validAccessToken(presentingWindow: NSWindow?) async throws -> String {
@@ -140,9 +174,13 @@ final class GoogleOAuthService: NSObject {
         let loopback = try await OAuthLoopbackServer.start()
         let loopback = try await OAuthLoopbackServer.start()
         defer { loopback.stop() }
         defer { loopback.stop() }
         let redirectURI = loopback.redirectURI
         let redirectURI = loopback.redirectURI
+        
+        let lastEmailHint = UserDefaults.standard
+            .string(forKey: lastSignedInEmailDefaultsKey)?
+            .trimmingCharacters(in: .whitespacesAndNewlines)
 
 
         var components = URLComponents(string: "https://accounts.google.com/o/oauth2/v2/auth")!
         var components = URLComponents(string: "https://accounts.google.com/o/oauth2/v2/auth")!
-        components.queryItems = [
+        var queryItems: [URLQueryItem] = [
             URLQueryItem(name: "client_id", value: clientId),
             URLQueryItem(name: "client_id", value: clientId),
             URLQueryItem(name: "redirect_uri", value: redirectURI),
             URLQueryItem(name: "redirect_uri", value: redirectURI),
             URLQueryItem(name: "response_type", value: "code"),
             URLQueryItem(name: "response_type", value: "code"),
@@ -152,19 +190,21 @@ final class GoogleOAuthService: NSObject {
             URLQueryItem(name: "state", value: state),
             URLQueryItem(name: "state", value: state),
             URLQueryItem(name: "code_challenge", value: codeChallenge),
             URLQueryItem(name: "code_challenge", value: codeChallenge),
             URLQueryItem(name: "code_challenge_method", value: "S256"),
             URLQueryItem(name: "code_challenge_method", value: "S256"),
-            URLQueryItem(name: "access_type", value: "offline")
+            URLQueryItem(name: "access_type", value: "offline"),
+            // Prefer the same Google account when multiple are present, and if none are signed in
+            // this forces Google to show sign-in UI.
+            URLQueryItem(name: "prompt", value: "select_account")
         ]
         ]
+        if let lastEmailHint, lastEmailHint.isEmpty == false {
+            queryItems.append(URLQueryItem(name: "login_hint", value: lastEmailHint))
+        }
+        components.queryItems = queryItems
 
 
         guard let authURL = components.url else { throw GoogleOAuthError.invalidCallbackURL }
         guard let authURL = components.url else { throw GoogleOAuthError.invalidCallbackURL }
         let opened = await MainActor.run { [self] in
         let opened = await MainActor.run { [self] in
-            openAuthURLInApp(authURL, presentingWindow: presentingWindow)
+            openAuthURLInDefaultBrowser(authURL)
         }
         }
         guard opened else { throw GoogleOAuthError.unableToOpenBrowser }
         guard opened else { throw GoogleOAuthError.unableToOpenBrowser }
-        defer {
-            Task { @MainActor [weak self] in
-                self?.closeInAppOAuthWindow()
-            }
-        }
         let callbackURL = try await loopback.waitForCallback()
         let callbackURL = try await loopback.waitForCallback()
 
 
         guard let returnedState = URLComponents(url: callbackURL, resolvingAgainstBaseURL: false)?
         guard let returnedState = URLComponents(url: callbackURL, resolvingAgainstBaseURL: false)?
@@ -189,21 +229,8 @@ final class GoogleOAuthService: NSObject {
     }
     }
 
 
     @MainActor
     @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
+    private func openAuthURLInDefaultBrowser(_ url: URL) -> Bool {
+        NSWorkspace.shared.open(url)
     }
     }
 
 
     @MainActor
     @MainActor

+ 2 - 1
meetings_app/StatusBar/StatusBarController.swift

@@ -130,7 +130,8 @@ final class StatusBarController: NSObject {
     @objc private func meetingClicked(_ sender: NSMenuItem) {
     @objc private func meetingClicked(_ sender: NSMenuItem) {
         guard let link = sender.representedObject as? String,
         guard let link = sender.representedObject as? String,
               let url = URL(string: link.trimmingCharacters(in: .whitespacesAndNewlines)) else { return }
               let url = URL(string: link.trimmingCharacters(in: .whitespacesAndNewlines)) else { return }
-        NSWorkspace.shared.open(url)
+        let routed = authService.urlForPreferredGoogleAccountIfPossible(url)
+        NSWorkspace.shared.open(routed)
     }
     }
 
 
     @objc private func quitClicked() {
     @objc private func quitClicked() {

+ 3 - 2
meetings_app/ViewController.swift

@@ -1076,7 +1076,8 @@ private extension ViewController {
     }
     }
 
 
     private func openInDefaultBrowser(url: URL) {
     private func openInDefaultBrowser(url: URL) {
-        NSWorkspace.shared.open(url, configuration: NSWorkspace.OpenConfiguration()) { [weak self] _, error in
+        let routed = googleOAuth.urlForPreferredGoogleAccountIfPossible(url)
+        NSWorkspace.shared.open(routed, configuration: NSWorkspace.OpenConfiguration()) { [weak self] _, error in
             if let error {
             if let error {
                 DispatchQueue.main.async {
                 DispatchQueue.main.async {
                     self?.showSimpleAlert(title: "Unable to open browser", message: error.localizedDescription)
                     self?.showSimpleAlert(title: "Unable to open browser", message: error.localizedDescription)
@@ -9534,7 +9535,7 @@ private extension ViewController {
         let title = meeting?.title ?? "Scheduled Meeting"
         let title = meeting?.title ?? "Scheduled Meeting"
         let shouldOpenMeeting = beginMeetingRecordingIfConsented(meetingTitle: title, meetingURL: url)
         let shouldOpenMeeting = beginMeetingRecordingIfConsented(meetingTitle: title, meetingURL: url)
         guard shouldOpenMeeting else { return }
         guard shouldOpenMeeting else { return }
-        NSWorkspace.shared.open(url)
+        openInDefaultBrowser(url: url)
     }
     }
 
 
     private func renderScheduleCards(into stack: NSStackView, meetings: [ScheduledMeeting]) {
     private func renderScheduleCards(into stack: NSStackView, meetings: [ScheduledMeeting]) {