Переглянути джерело

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 1 місяць тому
батько
коміт
889274c388

+ 51 - 24
meetings_app/Auth/GoogleOAuthService.swift

@@ -55,6 +55,35 @@ final class GoogleOAuthService: NSObject {
55 55
     private let tokenStore = KeychainTokenStore()
56 56
     @MainActor private var inAppOAuthWindowController: InAppOAuthWindowController?
57 57
     private override init() {}
58
+    
59
+    private let lastSignedInEmailDefaultsKey = "google.oauth.lastSignedInEmail"
60
+    
61
+    func lastSignedInEmailHint() -> String? {
62
+        let value = UserDefaults.standard
63
+            .string(forKey: lastSignedInEmailDefaultsKey)?
64
+            .trimmingCharacters(in: .whitespacesAndNewlines)
65
+        guard let value, value.isEmpty == false else { return nil }
66
+        return value.lowercased()
67
+    }
68
+    
69
+    /// Wraps a Google URL so the default browser is nudged to use the same account as the app.
70
+    /// If the browser isn't signed into that account, Google will prompt to sign in.
71
+    func urlForPreferredGoogleAccountIfPossible(_ url: URL) -> URL {
72
+        guard let email = lastSignedInEmailHint() else { return url }
73
+        guard let scheme = url.scheme?.lowercased(), scheme == "https" || scheme == "http" else { return url }
74
+        guard let host = url.host?.lowercased(), host.isEmpty == false else { return url }
75
+        
76
+        // Only attempt for Google properties where account mixups are common.
77
+        let isGoogle = host == "google.com" || host.hasSuffix(".google.com")
78
+        guard isGoogle else { return url }
79
+        
80
+        var components = URLComponents(string: "https://accounts.google.com/AccountChooser")!
81
+        components.queryItems = [
82
+            URLQueryItem(name: "continue", value: url.absoluteString),
83
+            URLQueryItem(name: "Email", value: email)
84
+        ]
85
+        return components.url ?? url
86
+    }
58 87
 
59 88
     func configuredClientId() -> String? {
60 89
         let value = UserDefaults.standard.string(forKey: clientIdDefaultsKey)?.trimmingCharacters(in: .whitespacesAndNewlines)
@@ -108,7 +137,12 @@ final class GoogleOAuthService: NSObject {
108 137
             let details = String(data: data, encoding: .utf8) ?? "HTTP \((response as? HTTPURLResponse)?.statusCode ?? -1)"
109 138
             throw GoogleOAuthError.tokenExchangeFailed(details)
110 139
         }
111
-        return try JSONDecoder().decode(GoogleUserProfile.self, from: data)
140
+        let profile = try JSONDecoder().decode(GoogleUserProfile.self, from: data)
141
+        let cleaned = profile.email?.trimmingCharacters(in: .whitespacesAndNewlines)
142
+        if let cleaned, cleaned.isEmpty == false {
143
+            UserDefaults.standard.set(cleaned.lowercased(), forKey: lastSignedInEmailDefaultsKey)
144
+        }
145
+        return profile
112 146
     }
113 147
 
114 148
     func validAccessToken(presentingWindow: NSWindow?) async throws -> String {
@@ -140,9 +174,13 @@ final class GoogleOAuthService: NSObject {
140 174
         let loopback = try await OAuthLoopbackServer.start()
141 175
         defer { loopback.stop() }
142 176
         let redirectURI = loopback.redirectURI
177
+        
178
+        let lastEmailHint = UserDefaults.standard
179
+            .string(forKey: lastSignedInEmailDefaultsKey)?
180
+            .trimmingCharacters(in: .whitespacesAndNewlines)
143 181
 
144 182
         var components = URLComponents(string: "https://accounts.google.com/o/oauth2/v2/auth")!
145
-        components.queryItems = [
183
+        var queryItems: [URLQueryItem] = [
146 184
             URLQueryItem(name: "client_id", value: clientId),
147 185
             URLQueryItem(name: "redirect_uri", value: redirectURI),
148 186
             URLQueryItem(name: "response_type", value: "code"),
@@ -152,19 +190,21 @@ final class GoogleOAuthService: NSObject {
152 190
             URLQueryItem(name: "state", value: state),
153 191
             URLQueryItem(name: "code_challenge", value: codeChallenge),
154 192
             URLQueryItem(name: "code_challenge_method", value: "S256"),
155
-            URLQueryItem(name: "access_type", value: "offline")
193
+            URLQueryItem(name: "access_type", value: "offline"),
194
+            // Prefer the same Google account when multiple are present, and if none are signed in
195
+            // this forces Google to show sign-in UI.
196
+            URLQueryItem(name: "prompt", value: "select_account")
156 197
         ]
198
+        if let lastEmailHint, lastEmailHint.isEmpty == false {
199
+            queryItems.append(URLQueryItem(name: "login_hint", value: lastEmailHint))
200
+        }
201
+        components.queryItems = queryItems
157 202
 
158 203
         guard let authURL = components.url else { throw GoogleOAuthError.invalidCallbackURL }
159 204
         let opened = await MainActor.run { [self] in
160
-            openAuthURLInApp(authURL, presentingWindow: presentingWindow)
205
+            openAuthURLInDefaultBrowser(authURL)
161 206
         }
162 207
         guard opened else { throw GoogleOAuthError.unableToOpenBrowser }
163
-        defer {
164
-            Task { @MainActor [weak self] in
165
-                self?.closeInAppOAuthWindow()
166
-            }
167
-        }
168 208
         let callbackURL = try await loopback.waitForCallback()
169 209
 
170 210
         guard let returnedState = URLComponents(url: callbackURL, resolvingAgainstBaseURL: false)?
@@ -189,21 +229,8 @@ final class GoogleOAuthService: NSObject {
189 229
     }
190 230
 
191 231
     @MainActor
192
-    private func openAuthURLInApp(_ url: URL, presentingWindow: NSWindow?) -> Bool {
193
-        let controller: InAppOAuthWindowController
194
-        if let existing = inAppOAuthWindowController {
195
-            controller = existing
196
-        } else {
197
-            controller = InAppOAuthWindowController()
198
-            inAppOAuthWindowController = controller
199
-        }
200
-        controller.alignWithPresentingWindow(presentingWindow)
201
-        controller.load(url: url)
202
-        controller.showWindow(nil)
203
-        controller.window?.makeKeyAndOrderFront(nil)
204
-        controller.window?.orderFrontRegardless()
205
-        NSApp.activate(ignoringOtherApps: true)
206
-        return true
232
+    private func openAuthURLInDefaultBrowser(_ url: URL) -> Bool {
233
+        NSWorkspace.shared.open(url)
207 234
     }
208 235
 
209 236
     @MainActor

+ 2 - 1
meetings_app/StatusBar/StatusBarController.swift

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

+ 3 - 2
meetings_app/ViewController.swift

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